sanjang 0.3.3 → 0.3.5

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.
@@ -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>
@@ -101,11 +77,17 @@
101
77
  </div>
102
78
  <div style="flex:1"></div>
103
79
  <button class="btn btn-sub btn-sm" id="ws-terminal-btn" onclick="wsOpenTerminal()">💻 터미널</button>
80
+ <button class="btn btn-sub btn-sm" id="ws-compare-btn" onclick="toggleCompare()" title="원본과 비교">🔀 비교</button>
104
81
  <button class="btn btn-sub btn-sm" onclick="togglePanel()">⛰ 패널</button>
105
82
  </div>
106
83
 
107
- <!-- Preview — full screen -->
108
- <div class="ws-preview-full" id="ws-preview"></div>
84
+ <!-- Preview — full screen with optional split -->
85
+ <div class="ws-preview-container" id="ws-preview-container">
86
+ <div class="ws-preview-full" id="ws-preview"></div>
87
+ <div class="ws-preview-main hidden" id="ws-preview-main">
88
+ <div class="ws-preview-label">🏔️ 원본 (main)</div>
89
+ </div>
90
+ </div>
109
91
 
110
92
  <!-- Slide panel — right side -->
111
93
  <div class="ws-panel" id="ws-panel">
@@ -124,6 +106,12 @@
124
106
  <div id="ws-changes"></div>
125
107
  </details>
126
108
  </div>
109
+ <div class="workspace-section ws-report-section" id="ws-report-section" style="display:none">
110
+ <h3>📋 변경 리포트</h3>
111
+ <div class="ws-report-summary" id="ws-report-summary"></div>
112
+ <div class="ws-report-warnings" id="ws-report-warnings"></div>
113
+ <div class="ws-report-categories" id="ws-report-categories"></div>
114
+ </div>
127
115
  <div class="workspace-section">
128
116
  <h3>📜 세이브 기록</h3>
129
117
  <div id="ws-actions"></div>
@@ -133,6 +121,7 @@
133
121
  <div class="ws-browser-error-panel" id="ws-browser-errors">
134
122
  <span style="color:var(--text-muted);font-size:12px">에러 없음</span>
135
123
  </div>
124
+ <button class="btn btn-fix" id="ws-fix-btn" onclick="copyFixPrompt()" style="display:none">🩹 고쳐줘 — 클립보드 복사</button>
136
125
  </details>
137
126
  <details class="workspace-section ws-log-details">
138
127
  <summary>📜 로그</summary>
@@ -243,6 +232,7 @@
243
232
  <p id="ship-file-count" style="font-size:13px;color:var(--text-muted);margin-bottom:12px"></p>
244
233
 
245
234
  <div class="form-group">
235
+ <div id="ship-report-preview" class="ship-report-preview" style="display:none"></div>
246
236
  <label class="form-label" for="ship-message">뭘 바꿨나요? (한 줄로)</label>
247
237
  <input
248
238
  class="form-input"
@@ -1090,6 +1090,23 @@ header h1::before {
1090
1090
  word-break: break-all;
1091
1091
  }
1092
1092
 
1093
+ .btn-fix {
1094
+ display: block;
1095
+ width: 100%;
1096
+ margin-top: 8px;
1097
+ padding: 8px 12px;
1098
+ background: linear-gradient(135deg, #ef4444 0%, #f97316 100%);
1099
+ color: #fff;
1100
+ border: none;
1101
+ border-radius: 6px;
1102
+ font-size: 13px;
1103
+ font-weight: 600;
1104
+ cursor: pointer;
1105
+ transition: opacity 0.15s, transform 0.1s;
1106
+ }
1107
+ .btn-fix:hover { opacity: 0.9; transform: translateY(-1px); }
1108
+ .btn-fix:active { transform: translateY(0); }
1109
+
1093
1110
  .ws-error-badge {
1094
1111
  background: #ef4444;
1095
1112
  color: white;
@@ -1707,11 +1724,8 @@ header.hidden {
1707
1724
  margin-top: 8px;
1708
1725
  }
1709
1726
  .ws-commit-item {
1710
- display: flex;
1711
- justify-content: space-between;
1712
- align-items: baseline;
1713
- gap: 8px;
1714
- padding: 4px 0;
1727
+ display: block;
1728
+ padding: 0;
1715
1729
  font-size: 13px;
1716
1730
  border-bottom: 1px solid var(--border);
1717
1731
  }
@@ -1720,6 +1734,10 @@ header.hidden {
1720
1734
  }
1721
1735
  .ws-commit-msg {
1722
1736
  color: var(--text-primary);
1737
+ white-space: nowrap;
1738
+ overflow: hidden;
1739
+ text-overflow: ellipsis;
1740
+ min-width: 0;
1723
1741
  }
1724
1742
  .ws-commit-date {
1725
1743
  color: var(--text-muted);
@@ -2046,331 +2064,73 @@ header.hidden {
2046
2064
  Onboarding Tutorial
2047
2065
  ============================================================ */
2048
2066
 
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
2067
 
2114
2068
  /* ============================================================
2115
- Basecamp Scene
2069
+ Basecamp Scene — Himalaya
2116
2070
  ============================================================ */
2117
2071
 
2118
2072
  .bc-scene {
2119
2073
  position: relative;
2120
- height: 200px;
2121
- background: linear-gradient(180deg, #080a10 0%, #0c0e14 60%, #12151e 100%);
2074
+ height: 220px;
2122
2075
  border-radius: 12px;
2123
2076
  overflow: hidden;
2124
2077
  border: 1px solid #1c2030;
2125
2078
  }
2126
2079
 
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;
2080
+ .bc-scene svg {
2081
+ display: block;
2082
+ width: 100%;
2083
+ height: 100%;
2190
2084
  }
2191
2085
 
2192
- /* Campfire glow */
2193
- .bc-glow {
2086
+ /* Speech bubble positioning */
2087
+ .bc-speech-wrap {
2194
2088
  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; }
2089
+ bottom: 56px;
2090
+ left: calc(50% + 16px);
2091
+ z-index: 5;
2207
2092
  }
2208
2093
 
2209
- /* Campfire pixel */
2210
- .bc-campfire {
2094
+ .bc-sherpa-hitbox {
2211
2095
  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
- }
2096
+ bottom: -44px;
2097
+ left: -8px;
2098
+ width: 44px;
2099
+ height: 44px;
2100
+ cursor: pointer;
2101
+ z-index: 6;
2257
2102
  }
2258
2103
 
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
- }
2104
+ #basecamp-scene {
2105
+ position: relative;
2347
2106
  }
2348
2107
 
2349
- /* Speech bubble */
2350
2108
  .bc-speech {
2351
- position: absolute;
2352
- bottom: 52px;
2353
- left: calc(50% + 8px);
2109
+ position: relative;
2354
2110
  background: #1c2030;
2355
2111
  border: 1px solid #2a2f42;
2356
2112
  border-radius: 8px;
2357
2113
  padding: 6px 10px;
2358
2114
  font-size: 11px;
2115
+ color: #e4e8f0;
2359
2116
  white-space: nowrap;
2360
- color: var(--text-secondary);
2117
+ max-width: min(320px, calc(100vw - 120px));
2118
+ overflow: hidden;
2119
+ text-overflow: ellipsis;
2361
2120
  animation: bubble-float 3s ease-in-out infinite;
2362
2121
  }
2363
2122
 
2364
2123
  .bc-speech::after {
2365
2124
  content: '';
2366
2125
  position: absolute;
2367
- bottom: -6px;
2126
+ bottom: -5px;
2368
2127
  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;
2128
+ width: 8px;
2129
+ height: 8px;
2130
+ background: #1c2030;
2131
+ border-right: 1px solid #2a2f42;
2132
+ border-bottom: 1px solid #2a2f42;
2133
+ transform: rotate(45deg);
2374
2134
  }
2375
2135
 
2376
2136
  @keyframes bubble-float {
@@ -2388,6 +2148,20 @@ header.hidden {
2388
2148
  transition: opacity 0.5s;
2389
2149
  }
2390
2150
 
2151
+ .bc-speech.guide-mode {
2152
+ border-color: rgba(99, 102, 241, 0.4);
2153
+ background: #1a1d2e;
2154
+ }
2155
+
2156
+ .bc-speech.guide-mode::after {
2157
+ background: #1a1d2e;
2158
+ border-color: rgba(99, 102, 241, 0.4);
2159
+ }
2160
+
2161
+ #sherpa-quote {
2162
+ transition: opacity 0.5s;
2163
+ }
2164
+
2391
2165
  /* ============================================================
2392
2166
  Activity Trail
2393
2167
  ============================================================ */
@@ -2416,3 +2190,60 @@ header.hidden {
2416
2190
  font-size: 11px;
2417
2191
  color: var(--text-muted);
2418
2192
  }
2193
+
2194
+ /* Change Report */
2195
+ .ws-report-section { border-top: 1px solid var(--border); padding-top: 12px; }
2196
+ .ws-report-desc { font-size: 14px; line-height: 1.6; color: var(--text); margin-bottom: 8px; white-space: pre-line; }
2197
+ .ws-report-warning { display: flex; align-items: center; gap: 6px; padding: 6px 8px; margin-bottom: 4px; background: rgba(255, 180, 0, 0.08); border-radius: 6px; font-size: 13px; color: var(--text-muted); }
2198
+ .ws-report-warning-icon { font-size: 14px; flex-shrink: 0; }
2199
+ .ws-report-categories { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
2200
+ .ws-report-cat-group { }
2201
+ .ws-report-cat-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
2202
+ .ws-report-cat-label { font-size: 13px; font-weight: 600; color: var(--text); }
2203
+ .ws-report-cat-count { font-size: 11px; color: var(--text-muted); background: var(--surface); padding: 1px 6px; border-radius: 8px; }
2204
+ .ws-report-cat-items { list-style: none; margin: 0; padding: 0; }
2205
+ .ws-report-cat-items li { font-size: 13px; color: var(--text-muted); padding: 2px 0 2px 16px; position: relative; line-height: 1.5; }
2206
+ .ws-report-cat-items li::before { content: '•'; position: absolute; left: 4px; color: var(--text-muted); }
2207
+ .ws-report-nav-item { cursor: pointer; transition: color 0.15s, background 0.15s; border-radius: 4px; }
2208
+ .ws-report-nav-item:hover { color: var(--accent) !important; background: color-mix(in srgb, var(--accent) 10%, transparent); }
2209
+ .ws-report-nav-hint { font-size: 11px; color: var(--accent); opacity: 0; transition: opacity 0.15s; margin-left: 4px; }
2210
+ .ws-report-nav-item:hover .ws-report-nav-hint { opacity: 1; }
2211
+ .ws-report-saved { opacity: 0.7; }
2212
+ .ws-report-saved-desc { font-size: 13px; color: var(--text-muted); }
2213
+
2214
+ /* Commit report (details/summary dropdown) */
2215
+ details.ws-commit-item { border-radius: 6px; margin-bottom: 4px; }
2216
+ details.ws-commit-item > summary.ws-commit-summary { display: flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; padding: 6px 8px; border-radius: 6px; flex-wrap: nowrap; }
2217
+ details.ws-commit-item > summary.ws-commit-summary::-webkit-details-marker { display: none; }
2218
+ details.ws-commit-item > summary.ws-commit-summary:hover { background: var(--surface); }
2219
+ .ws-commit-arrow { font-size: 10px; color: var(--text-muted); flex-shrink: 0; transition: transform 0.15s; display: inline-block; }
2220
+ details.ws-commit-item[open] .ws-commit-arrow { transform: rotate(90deg); }
2221
+ .ws-commit-report { margin: 4px 8px 8px 8px; padding: 10px 12px; background: var(--surface); border-radius: 6px; font-size: 12px; width: auto; }
2222
+ .ws-commit-report-loading, .ws-commit-report-empty { color: var(--text-muted); }
2223
+ .ws-commit-cat { margin-bottom: 6px; }
2224
+ .ws-commit-cat:last-child { margin-bottom: 0; }
2225
+ .ws-commit-cat-label { font-size: 11px; font-weight: 600; color: var(--text-muted); display: block; margin-bottom: 2px; }
2226
+ .ws-commit-cat-item { color: var(--text-muted); padding-left: 12px; position: relative; line-height: 1.5; }
2227
+ .ws-commit-cat-item::before { content: '•'; position: absolute; left: 3px; color: var(--text-muted); }
2228
+
2229
+ /* Split-view compare */
2230
+ .ws-preview-container { position: relative; flex: 1; display: flex; }
2231
+ .ws-preview-container .ws-preview-full { flex: 1; }
2232
+ .ws-preview-main { display: none; flex: 1; flex-direction: column; border-left: 2px solid var(--accent); position: relative; }
2233
+ .ws-preview-main.hidden { display: none !important; }
2234
+ .ws-split-view .ws-preview-full,
2235
+ .ws-split-view .ws-preview-main { flex: 1; display: flex; flex-direction: column; }
2236
+ .ws-preview-label { position: absolute; top: 8px; left: 8px; z-index: 10; background: var(--surface); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--text-muted); pointer-events: none; }
2237
+ .ws-preview-loading { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-muted); font-size: 14px; }
2238
+ .btn-active { background: var(--accent) !important; color: white !important; }
2239
+ .ws-split-view .ws-preview-full { position: relative; }
2240
+ .ws-split-view .ws-preview-full::before {
2241
+ content: '⛺ 내 캠프';
2242
+ position: absolute; top: 8px; left: 8px; z-index: 10;
2243
+ background: var(--surface); padding: 2px 8px; border-radius: 4px;
2244
+ font-size: 11px; color: var(--text-muted); pointer-events: none;
2245
+ }
2246
+
2247
+ /* Ship report preview */
2248
+ .ship-report-preview { margin-bottom: 12px; padding: 10px; background: var(--surface); border-radius: 6px; font-size: 13px; line-height: 1.5; }
2249
+ .ship-report-desc { margin-bottom: 8px; }
@@ -14,7 +14,7 @@ for (let i = 0; i < args.length; i++) {
14
14
  i++;
15
15
  }
16
16
  if (args[i] === "--port" && args[i + 1]) {
17
- port = parseInt(args[i + 1]);
17
+ port = parseInt(args[i + 1], 10);
18
18
  i++;
19
19
  }
20
20
  if (args[i] === "--force") {
@@ -53,8 +53,8 @@ if (command === "init") {
53
53
  rl.question(" 어떤 앱을 띄울까요? [번호]: ", resolve);
54
54
  });
55
55
  rl.close();
56
- const idx = parseInt(answer) - 1;
57
- if (idx < 0 || idx >= apps.length || isNaN(idx)) {
56
+ const idx = parseInt(answer, 10) - 1;
57
+ if (idx < 0 || idx >= apps.length || Number.isNaN(idx)) {
58
58
  console.error("⛰ 잘못된 선택입니다.");
59
59
  process.exit(1);
60
60
  }
@@ -132,7 +132,51 @@ 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) => {
151
+ rl.question(" 어떤 앱을 띄울까요? [번호]: ", r);
152
+ });
153
+ rl.close();
154
+ const idx = parseInt(answer, 10) - 1;
155
+ if (idx < 0 || idx >= apps.length || Number.isNaN(idx)) {
156
+ console.error("⛰ 잘못된 선택입니다.");
157
+ process.exit(1);
158
+ }
159
+ appDir = apps[idx].dir;
160
+ console.log(` → ${appDir}/ (${apps[idx].framework}) 선택됨\n`);
161
+ }
162
+ else if (apps.length === 1) {
163
+ appDir = apps[0].dir;
164
+ }
165
+ const result = generateConfig(projectRoot, { appDir, force });
166
+ if (result.created) {
167
+ console.log(`⛰ ${result.message}`);
168
+ console.log(` 프레임워크: ${result.framework}\n`);
169
+ }
170
+ // Add .sanjang to .gitignore
171
+ const gitignorePath = resolve(projectRoot, ".gitignore");
172
+ if (existsSync(gitignorePath)) {
173
+ const { readFileSync, appendFileSync } = await import("node:fs");
174
+ const content = readFileSync(gitignorePath, "utf8");
175
+ if (!content.includes(".sanjang")) {
176
+ appendFileSync(gitignorePath, "\n# Sanjang local dev camps\n.sanjang/\n");
177
+ }
178
+ }
179
+ }
136
180
  const { startServer } = await import("../lib/server.js");
137
181
  await startServer(projectRoot, { port });
138
182
  }
@@ -230,13 +230,11 @@ function detectTurboMainApp(root) {
230
230
  const port = portMatch?.[1] ? parseInt(portMatch[1], 10) : 3000;
231
231
  candidates.push({ name: entry.name, port });
232
232
  }
233
- catch {
234
- continue;
235
- }
233
+ catch { }
236
234
  }
237
235
  }
238
236
  // Prefer app with explicit port, then first candidate
239
- return candidates.find(c => c.port !== 3000) ?? candidates[0] ?? null;
237
+ return candidates.find((c) => c.port !== 3000) ?? candidates[0] ?? null;
240
238
  }
241
239
  function detectPackageManager(root) {
242
240
  if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")))
@@ -272,7 +270,7 @@ export function generateConfig(projectRoot, options = {}) {
272
270
  // Exclude .env.example, .env.test, .env.template
273
271
  (f) => !f.includes("example") && !f.includes("template") && !f.includes(".test"));
274
272
  // Detect potential issues
275
- const issues = detectSetupIssues(detectRoot);
273
+ const _issues = detectSetupIssues(detectRoot);
276
274
  const lines = [
277
275
  "export default {",
278
276
  ` // ${detected.framework} detected`,