sanjang 0.3.5 → 0.3.7

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.
@@ -12,7 +12,7 @@
12
12
  <!-- Header -->
13
13
  <header>
14
14
  <h1>산장 <span id="project-name" style="font-weight:400;color:var(--text-muted);font-size:0.6em"></span></h1>
15
- <button class="btn btn-primary" onclick="document.getElementById('quickstart-input').focus();document.getElementById('quickstart-input').scrollIntoView({behavior:'smooth'})">+ 새 캠프</button>
15
+ <button class="btn btn-primary" onclick="openNewModal()">+ 새 캠프</button>
16
16
  </header>
17
17
 
18
18
  <!-- Portal Home -->
@@ -31,9 +31,13 @@
31
31
  </div>
32
32
  </div>
33
33
 
34
+ <!-- Stale camp banner -->
35
+ <div class="portal-stale-banner" id="portal-stale-banner" style="display:none"></div>
36
+
34
37
  <!-- 새로 시작 (핵심 액션, 맨 위) -->
35
38
  <div class="portal-section">
36
39
  <div class="portal-quickstart">
40
+ <label for="quickstart-input" class="sr-only">뭘 하고 싶어?</label>
37
41
  <input class="form-input portal-quickstart-input" id="quickstart-input"
38
42
  type="text" placeholder="뭘 하고 싶어? (예: 로그인 버튼 색상 변경)"
39
43
  autocomplete="off" spellcheck="false"
@@ -65,7 +69,10 @@
65
69
  <!-- Top bar — minimal, over the preview -->
66
70
  <div class="ws-topbar">
67
71
  <button class="btn btn-ghost btn-sm" onclick="exitWorkspace()">← 목록</button>
68
- <span class="workspace-title" id="ws-title"></span>
72
+ <div class="ws-camp-switcher">
73
+ <button class="ws-camp-switch-btn" id="ws-title" onclick="toggleCampSwitcher()"></button>
74
+ <div class="ws-camp-dropdown" id="ws-camp-dropdown"></div>
75
+ </div>
69
76
  <span class="workspace-status" id="ws-status"></span>
70
77
  <div class="ws-mini-char" id="ws-mini-char"></div>
71
78
  <div class="ws-quest" id="ws-quest">
@@ -76,11 +83,27 @@
76
83
  <div class="ws-quest-step" id="ws-step-ship"><div class="ws-quest-dot"></div><span>보내기</span></div>
77
84
  </div>
78
85
  <div style="flex:1"></div>
86
+ <button class="btn btn-sub btn-sm" id="ws-test-btn" onclick="wsRunTest()">🧪 테스트</button>
79
87
  <button class="btn btn-sub btn-sm" id="ws-terminal-btn" onclick="wsOpenTerminal()">💻 터미널</button>
80
88
  <button class="btn btn-sub btn-sm" id="ws-compare-btn" onclick="toggleCompare()" title="원본과 비교">🔀 비교</button>
81
89
  <button class="btn btn-sub btn-sm" onclick="togglePanel()">⛰ 패널</button>
82
90
  </div>
83
91
 
92
+ <!-- Preview toolbar — URL bar + viewport switcher -->
93
+ <div class="ws-preview-toolbar" id="ws-preview-toolbar" style="display:none">
94
+ <div class="ws-url-bar">
95
+ <button class="ws-url-back" onclick="previewBack()" title="뒤로">←</button>
96
+ <button class="ws-url-refresh" onclick="previewRefresh()" title="새로고침">↻</button>
97
+ <input class="ws-url-input" id="ws-url-input" type="text" placeholder="/" spellcheck="false"
98
+ onkeydown="if(event.key==='Enter')navigatePreview(this.value)">
99
+ </div>
100
+ <div class="ws-viewport-switcher">
101
+ <button class="ws-vp-btn ws-vp-active" onclick="setViewport('desktop')" title="데스크탑">🖥</button>
102
+ <button class="ws-vp-btn" onclick="setViewport('tablet')" title="태블릿">📱</button>
103
+ <button class="ws-vp-btn" onclick="setViewport('mobile')" title="모바일">📲</button>
104
+ </div>
105
+ </div>
106
+
84
107
  <!-- Preview — full screen with optional split -->
85
108
  <div class="ws-preview-container" id="ws-preview-container">
86
109
  <div class="ws-preview-full" id="ws-preview"></div>
@@ -116,16 +139,39 @@
116
139
  <h3>📜 세이브 기록</h3>
117
140
  <div id="ws-actions"></div>
118
141
  </div>
119
- <details class="workspace-section ws-log-details">
120
- <summary>🔴 브라우저 에러 <span class="ws-error-badge" id="ws-browser-error-badge" style="display:none">0</span></summary>
121
- <div class="ws-browser-error-panel" id="ws-browser-errors">
122
- <span style="color:var(--text-muted);font-size:12px">에러 없음</span>
142
+ <details class="workspace-section ws-log-details" open>
143
+ <summary>🛠 개발자 도구</summary>
144
+ <div class="ws-devtabs">
145
+ <button class="ws-devtab-btn ws-devtab-active" data-tab="errors" onclick="switchDevTab('errors')">🔴 에러 <span class="ws-error-badge" id="ws-browser-error-badge" style="display:none">0</span></button>
146
+ <button class="ws-devtab-btn" data-tab="console" onclick="switchDevTab('console')">콘솔 <span class="ws-devtab-badge" id="ws-console-badge" style="display:none">0</span></button>
147
+ <button class="ws-devtab-btn" data-tab="network" onclick="switchDevTab('network')">네트워크 <span class="ws-devtab-badge" id="ws-network-badge" style="display:none">0</span></button>
148
+ <button class="ws-devtab-btn" data-tab="test" onclick="switchDevTab('test')">🧪 테스트 <span class="ws-devtab-badge ws-test-badge" id="ws-test-badge" style="display:none"></span></button>
149
+ <button class="ws-devtab-btn" data-tab="log" onclick="switchDevTab('log')">📜 로그</button>
150
+ </div>
151
+ <div class="ws-devtab-panel" id="ws-devtab-errors">
152
+ <div class="ws-browser-error-panel" id="ws-browser-errors">
153
+ <span style="color:var(--text-muted);font-size:12px">에러 없음</span>
154
+ </div>
155
+ <button class="btn btn-fix" id="ws-fix-btn" onclick="copyFixPrompt()" style="display:none">🩹 고쳐줘 — 클립보드 복사</button>
156
+ </div>
157
+ <div class="ws-devtab-panel" id="ws-devtab-console" style="display:none">
158
+ <div class="ws-console-panel" id="ws-console-panel">
159
+ <span style="color:var(--text-muted);font-size:12px">로그 없음</span>
160
+ </div>
161
+ </div>
162
+ <div class="ws-devtab-panel" id="ws-devtab-network" style="display:none">
163
+ <div class="ws-network-panel" id="ws-network-panel">
164
+ <span style="color:var(--text-muted);font-size:12px">요청 없음</span>
165
+ </div>
166
+ </div>
167
+ <div class="ws-devtab-panel" id="ws-devtab-test" style="display:none">
168
+ <div class="ws-test-panel" id="ws-test-panel">
169
+ <span style="color:var(--text-muted);font-size:12px">🧪 버튼을 눌러 테스트 실행</span>
170
+ </div>
171
+ </div>
172
+ <div class="ws-devtab-panel" id="ws-devtab-log" style="display:none">
173
+ <div class="ws-log-panel" id="ws-log"><pre></pre></div>
123
174
  </div>
124
- <button class="btn btn-fix" id="ws-fix-btn" onclick="copyFixPrompt()" style="display:none">🩹 고쳐줘 — 클립보드 복사</button>
125
- </details>
126
- <details class="workspace-section ws-log-details">
127
- <summary>📜 로그</summary>
128
- <div class="ws-log-panel" id="ws-log"><pre></pre></div>
129
175
  </details>
130
176
  </div>
131
177
  <div class="ws-panel-actions">
@@ -145,39 +191,68 @@
145
191
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="new-pg-modal-title">
146
192
  <h2 id="new-pg-modal-title">새 캠프</h2>
147
193
 
148
- <div class="form-group">
149
- <label class="form-label" for="new-pg-name">이름</label>
150
- <input
151
- class="form-input"
152
- id="new-pg-name"
153
- type="text"
154
- placeholder="예: my-feature"
155
- autocomplete="off"
156
- spellcheck="false"
157
- >
158
- <span class="form-hint">소문자, 숫자, 하이픈만 사용 가능</span>
159
- <span id="new-pg-name-error" style="color: var(--status-error-fg); font-size: 12px;"></span>
194
+ <div class="new-camp-tabs">
195
+ <button class="new-camp-tab active" data-tab="quick" onclick="switchNewCampTab('quick')">뭘 하고 싶어?</button>
196
+ <button class="new-camp-tab" data-tab="branch" onclick="switchNewCampTab('branch')">브랜치 선택</button>
160
197
  </div>
161
198
 
162
- <div class="form-group">
163
- <label class="form-label" for="new-pg-branch">브랜치</label>
164
- <div class="branch-picker" id="branch-picker">
199
+ <!-- 탭 1: 자연어 퀵스타트 -->
200
+ <div class="new-camp-panel" id="new-camp-quick">
201
+ <div class="form-group">
202
+ <label for="modal-quickstart-input" class="sr-only">뭘 하고 싶어?</label>
165
203
  <input
166
204
  class="form-input"
167
- id="new-pg-branch"
205
+ id="modal-quickstart-input"
168
206
  type="text"
169
- placeholder="브랜치 이름 검색..."
207
+ placeholder="예: 로그인 버튼 색상 변경"
170
208
  autocomplete="off"
171
209
  spellcheck="false"
210
+ onkeydown="if(event.key==='Enter')modalQuickStart()"
172
211
  >
173
- <div class="branch-dropdown" id="branch-dropdown"></div>
212
+ <span class="form-hint">AI가 브랜치 이름과 캠프를 자동으로 만들어줍니다</span>
213
+ </div>
214
+ <div class="modal-actions">
215
+ <button class="btn btn-ghost" onclick="closeNewModal()">취소</button>
216
+ <button class="btn btn-primary" id="modal-quickstart-btn" onclick="modalQuickStart()">시작</button>
174
217
  </div>
175
- <span class="form-hint" id="branch-count"></span>
176
218
  </div>
177
219
 
178
- <div class="modal-actions">
179
- <button class="btn btn-ghost" onclick="closeNewModal()">취소</button>
180
- <button class="btn btn-primary" id="create-pg-btn" onclick="createPg()">생성</button>
220
+ <!-- 탭 2: 기존 브랜치 선택 -->
221
+ <div class="new-camp-panel" id="new-camp-branch" style="display:none">
222
+ <div class="form-group">
223
+ <label class="form-label" for="new-pg-name">이름</label>
224
+ <input
225
+ class="form-input"
226
+ id="new-pg-name"
227
+ type="text"
228
+ placeholder="예: my-feature"
229
+ autocomplete="off"
230
+ spellcheck="false"
231
+ >
232
+ <span class="form-hint">소문자, 숫자, 하이픈만 사용 가능</span>
233
+ <span id="new-pg-name-error" style="color: var(--status-error-fg); font-size: 12px;"></span>
234
+ </div>
235
+
236
+ <div class="form-group">
237
+ <label class="form-label" for="new-pg-branch">브랜치</label>
238
+ <div class="branch-picker" id="branch-picker">
239
+ <input
240
+ class="form-input"
241
+ id="new-pg-branch"
242
+ type="text"
243
+ placeholder="브랜치 이름 검색..."
244
+ autocomplete="off"
245
+ spellcheck="false"
246
+ >
247
+ <div class="branch-dropdown" id="branch-dropdown"></div>
248
+ </div>
249
+ <span class="form-hint" id="branch-count"></span>
250
+ </div>
251
+
252
+ <div class="modal-actions">
253
+ <button class="btn btn-ghost" onclick="closeNewModal()">취소</button>
254
+ <button class="btn btn-primary" id="create-pg-btn" onclick="createPg()">생성</button>
255
+ </div>
181
256
  </div>
182
257
  </div>
183
258
  </div>
@@ -214,6 +289,19 @@
214
289
  </div>
215
290
  </div>
216
291
 
292
+ <!-- Stale Cleanup Modal -->
293
+ <div class="modal-backdrop" id="stale-modal">
294
+ <div class="modal" role="dialog" aria-modal="true">
295
+ <h2>캠프 정리</h2>
296
+ <p style="font-size:13px;color:var(--text-muted);margin-bottom:12px">삭제할 캠프를 선택하세요. 이 작업은 되돌릴 수 없습니다.</p>
297
+ <div id="stale-list"></div>
298
+ <div class="modal-actions">
299
+ <button class="btn btn-ghost" onclick="closeStaleModal()">취소</button>
300
+ <button class="btn btn-danger" onclick="confirmStaleCleanup()">삭제</button>
301
+ </div>
302
+ </div>
303
+ </div>
304
+
217
305
  <!-- Changes Modal -->
218
306
  <div class="modal-backdrop" id="changes-modal">
219
307
  <div class="modal" role="dialog" aria-modal="true">
@@ -225,6 +313,17 @@
225
313
  </div>
226
314
  </div>
227
315
 
316
+ <!-- Diff Modal -->
317
+ <div class="modal-backdrop" id="diff-modal">
318
+ <div class="modal modal-wide" role="dialog" aria-modal="true">
319
+ <h2 id="diff-modal-title">변경 내용</h2>
320
+ <div class="diff-content" id="diff-content"></div>
321
+ <div class="modal-actions">
322
+ <button class="btn btn-ghost" onclick="closeDiffModal()">닫기</button>
323
+ </div>
324
+ </div>
325
+ </div>
326
+
228
327
  <!-- Ship Modal -->
229
328
  <div class="modal-backdrop" id="ship-modal">
230
329
  <div class="modal" role="dialog" aria-modal="true">
@@ -270,13 +369,13 @@
270
369
 
271
370
  <!-- Conflict Modal -->
272
371
  <div class="modal-backdrop" id="conflict-modal">
273
- <div class="modal" role="dialog" aria-modal="true">
372
+ <div class="modal modal-wide" role="dialog" aria-modal="true">
274
373
  <h2>충돌이 발생했어요</h2>
275
374
  <p style="font-size:13px;color:var(--text-muted);margin-bottom:12px">
276
- 팀의 변경사항과 변경사항이 같은 부분을 수정해서 충돌이 생겼어요.
375
+ 같은 파일을 동시에 수정해서 충돌이 생겼어요. 파일별로 선택하거나, 한꺼번에 해결할 수 있어요.
277
376
  </p>
278
- <div id="conflict-files" style="margin-bottom:16px"></div>
279
- <p style="font-size:13px;margin-bottom:16px">어떻게 할까요?</p>
377
+ <div id="conflict-files" style="margin-bottom:16px;max-height:50vh;overflow:auto"></div>
378
+ <p style="font-size:13px;margin-bottom:16px">또는 한꺼번에:</p>
280
379
  <div style="display:flex;flex-direction:column;gap:8px">
281
380
  <button class="btn btn-primary" onclick="resolveConflict('claude')">
282
381
  Claude에게 맡기기 (추천)
@@ -7,6 +7,18 @@
7
7
 
8
8
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&display=swap');
9
9
 
10
+ .sr-only {
11
+ position: absolute;
12
+ width: 1px;
13
+ height: 1px;
14
+ padding: 0;
15
+ margin: -1px;
16
+ overflow: hidden;
17
+ clip: rect(0,0,0,0);
18
+ white-space: nowrap;
19
+ border: 0;
20
+ }
21
+
10
22
  *, *::before, *::after {
11
23
  box-sizing: border-box;
12
24
  margin: 0;
@@ -684,6 +696,35 @@ header h1::before {
684
696
  letter-spacing: -0.01em;
685
697
  }
686
698
 
699
+ .new-camp-tabs {
700
+ display: flex;
701
+ gap: 0;
702
+ border-bottom: 1px solid var(--border);
703
+ margin: -8px 0 0;
704
+ }
705
+
706
+ .new-camp-tab {
707
+ flex: 1;
708
+ padding: 8px 12px;
709
+ background: none;
710
+ border: none;
711
+ border-bottom: 2px solid transparent;
712
+ color: var(--text-muted);
713
+ font-size: 13px;
714
+ cursor: pointer;
715
+ transition: color 0.15s, border-color 0.15s;
716
+ }
717
+
718
+ .new-camp-tab:hover {
719
+ color: var(--text-primary);
720
+ }
721
+
722
+ .new-camp-tab.active {
723
+ color: var(--text-primary);
724
+ border-bottom-color: var(--accent);
725
+ font-weight: 500;
726
+ }
727
+
687
728
  /* Form elements */
688
729
  .form-group {
689
730
  display: flex;
@@ -968,6 +1009,49 @@ header h1::before {
968
1009
  font-weight: 600;
969
1010
  }
970
1011
 
1012
+ /* Camp switcher dropdown */
1013
+ .ws-camp-switcher { position: relative; }
1014
+ .ws-camp-switch-btn {
1015
+ background: none;
1016
+ border: 1px solid transparent;
1017
+ border-radius: 6px;
1018
+ padding: 4px 8px;
1019
+ font-size: 14px;
1020
+ font-weight: 600;
1021
+ color: var(--text);
1022
+ cursor: pointer;
1023
+ }
1024
+ .ws-camp-switch-btn:hover { border-color: var(--border-subtle); background: var(--surface); }
1025
+ .ws-camp-switch-btn::after { content: ' ▾'; font-size: 10px; color: var(--text-muted); }
1026
+ .ws-camp-dropdown {
1027
+ display: none;
1028
+ position: absolute;
1029
+ top: 100%;
1030
+ left: 0;
1031
+ z-index: 100;
1032
+ min-width: 220px;
1033
+ background: var(--bg);
1034
+ border: 1px solid var(--border-subtle);
1035
+ border-radius: 8px;
1036
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
1037
+ padding: 4px;
1038
+ margin-top: 4px;
1039
+ }
1040
+ .ws-camp-dropdown.open { display: block; }
1041
+ .ws-camp-dd-item {
1042
+ display: flex;
1043
+ align-items: center;
1044
+ gap: 8px;
1045
+ padding: 8px 10px;
1046
+ border-radius: 6px;
1047
+ cursor: pointer;
1048
+ font-size: 13px;
1049
+ }
1050
+ .ws-camp-dd-item:hover { background: var(--surface); }
1051
+ .ws-camp-dd-name { font-weight: 500; color: var(--text); }
1052
+ .ws-camp-dd-branch { color: var(--text-muted); font-size: 11px; margin-left: auto; }
1053
+ .ws-camp-dd-empty { padding: 12px; color: var(--text-muted); font-size: 12px; text-align: center; }
1054
+
971
1055
  /* Preview — full screen behind topbar */
972
1056
  .ws-preview-full {
973
1057
  position: absolute;
@@ -1074,6 +1158,7 @@ header h1::before {
1074
1158
  padding: 3px 0;
1075
1159
  border-bottom: 1px solid var(--border-subtle);
1076
1160
  display: flex;
1161
+ flex-wrap: wrap;
1077
1162
  gap: 6px;
1078
1163
  align-items: baseline;
1079
1164
  }
@@ -1090,6 +1175,32 @@ header h1::before {
1090
1175
  word-break: break-all;
1091
1176
  }
1092
1177
 
1178
+ .ws-error-stack {
1179
+ width: 100%;
1180
+ margin-top: 2px;
1181
+ }
1182
+
1183
+ .ws-error-stack summary {
1184
+ cursor: pointer;
1185
+ color: var(--text-muted);
1186
+ font-size: 10px;
1187
+ user-select: none;
1188
+ }
1189
+
1190
+ .ws-error-stack pre {
1191
+ margin: 4px 0 0;
1192
+ padding: 6px 8px;
1193
+ background: rgba(0,0,0,0.3);
1194
+ border-radius: 4px;
1195
+ font-size: 10px;
1196
+ line-height: 1.5;
1197
+ color: var(--text-secondary);
1198
+ white-space: pre-wrap;
1199
+ word-break: break-all;
1200
+ max-height: 150px;
1201
+ overflow-y: auto;
1202
+ }
1203
+
1093
1204
  .btn-fix {
1094
1205
  display: block;
1095
1206
  width: 100%;
@@ -1107,13 +1218,70 @@ header h1::before {
1107
1218
  .btn-fix:hover { opacity: 0.9; transform: translateY(-1px); }
1108
1219
  .btn-fix:active { transform: translateY(0); }
1109
1220
 
1110
- .ws-error-badge {
1221
+ .ws-error-badge, .ws-devtab-badge {
1111
1222
  background: #ef4444;
1112
1223
  color: white;
1113
1224
  font-size: 10px;
1114
1225
  padding: 1px 6px;
1115
1226
  border-radius: 10px;
1116
- margin-left: 6px;
1227
+ margin-left: 4px;
1228
+ }
1229
+
1230
+ /* Devtools tabs */
1231
+ .ws-devtabs {
1232
+ display: flex;
1233
+ gap: 0;
1234
+ border-bottom: 1px solid var(--border-subtle);
1235
+ margin-bottom: 8px;
1236
+ }
1237
+ .ws-devtab-btn {
1238
+ background: none;
1239
+ border: none;
1240
+ border-bottom: 2px solid transparent;
1241
+ padding: 6px 10px;
1242
+ font-size: 12px;
1243
+ color: var(--text-muted);
1244
+ cursor: pointer;
1245
+ white-space: nowrap;
1246
+ }
1247
+ .ws-devtab-btn:hover { color: var(--text); }
1248
+ .ws-devtab-btn.ws-devtab-active { color: var(--text); border-bottom-color: var(--accent); }
1249
+ .ws-devtab-panel { max-height: 300px; overflow-y: auto; }
1250
+
1251
+ /* Console panel */
1252
+ .ws-console-panel { padding: 4px 8px; font-size: 11px; font-family: var(--font-mono); }
1253
+ .ws-console-item { padding: 2px 0; border-bottom: 1px solid var(--border-subtle); word-break: break-all; }
1254
+ .ws-console-log { color: var(--text); }
1255
+ .ws-console-warn { color: #f59e0b; }
1256
+ .ws-console-info { color: #3b82f6; }
1257
+
1258
+ /* Network panel */
1259
+ .ws-network-panel { padding: 4px 8px; font-size: 11px; font-family: var(--font-mono); }
1260
+ .ws-net-item { display: flex; align-items: center; gap: 6px; padding: 3px 0; border-bottom: 1px solid var(--border-subtle); }
1261
+ .ws-net-method { font-weight: 600; font-size: 10px; padding: 1px 4px; border-radius: 3px; flex-shrink: 0; }
1262
+ .ws-net-method-get { color: #22c55e; }
1263
+ .ws-net-method-post { color: #3b82f6; }
1264
+ .ws-net-method-put { color: #f59e0b; }
1265
+ .ws-net-method-delete { color: #ef4444; }
1266
+ .ws-net-method-patch { color: #a855f7; }
1267
+ .ws-net-url { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
1268
+ .ws-net-status { font-weight: 600; flex-shrink: 0; }
1269
+ .ws-net-ok { color: #22c55e; }
1270
+ .ws-net-warn { color: #f59e0b; }
1271
+ .ws-net-err { color: #ef4444; }
1272
+ .ws-net-dur { color: var(--text-muted); flex-shrink: 0; }
1273
+
1274
+ /* Test runner panel */
1275
+ .ws-test-panel { padding: 4px 8px; font-size: 12px; }
1276
+ .ws-test-status { padding: 8px; font-weight: 600; border-radius: 6px; margin-bottom: 8px; }
1277
+ .ws-test-running { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
1278
+ .ws-test-pass { background: #dcfce7; color: #166534; }
1279
+ .ws-test-fail { background: #fef2f2; color: #991b1b; }
1280
+ .ws-test-output { max-height: 250px; overflow: auto; font-family: var(--font-mono); font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; padding: 8px; background: var(--surface); border-radius: 6px; }
1281
+ .ws-test-badge { font-size: 10px; }
1282
+ @media (prefers-color-scheme: dark) {
1283
+ .ws-test-pass { background: #052e16; color: #86efac; }
1284
+ .ws-test-fail { background: #450a0a; color: #fca5a5; }
1117
1285
  }
1118
1286
 
1119
1287
  .ws-file-item {
@@ -1123,6 +1291,62 @@ header h1::before {
1123
1291
  padding: 3px 0;
1124
1292
  font-size: 13px;
1125
1293
  }
1294
+ /* Stale camp banner + cleanup */
1295
+ .portal-stale-banner {
1296
+ display: flex;
1297
+ align-items: center;
1298
+ justify-content: space-between;
1299
+ padding: 10px 16px;
1300
+ margin: 0 16px;
1301
+ background: color-mix(in srgb, #f59e0b 12%, transparent);
1302
+ border: 1px solid color-mix(in srgb, #f59e0b 30%, transparent);
1303
+ border-radius: 8px;
1304
+ font-size: 13px;
1305
+ color: var(--text);
1306
+ }
1307
+ .ws-stale-item {
1308
+ display: flex;
1309
+ align-items: center;
1310
+ gap: 8px;
1311
+ padding: 8px 0;
1312
+ border-bottom: 1px solid var(--border-subtle);
1313
+ font-size: 13px;
1314
+ cursor: pointer;
1315
+ }
1316
+ .ws-stale-item:last-child { border-bottom: none; }
1317
+ .ws-stale-item input { flex-shrink: 0; }
1318
+
1319
+ /* Conflict resolution */
1320
+ .conflict-file { border: 1px solid var(--border-subtle); border-radius: 8px; margin-bottom: 8px; overflow: hidden; transition: opacity 0.3s; }
1321
+ .conflict-file-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: var(--surface); }
1322
+ .conflict-file-header code { font-size: 12px; }
1323
+ .conflict-file-actions { display: flex; gap: 4px; }
1324
+ .conflict-section { display: flex; gap: 0; }
1325
+ .conflict-side { flex: 1; padding: 8px; font-size: 11px; overflow: auto; }
1326
+ .conflict-side pre { margin: 0; white-space: pre-wrap; word-break: break-all; font-family: var(--font-mono); font-size: 11px; }
1327
+ .conflict-side-label { font-size: 10px; font-weight: 600; margin-bottom: 4px; }
1328
+ .conflict-ours { background: color-mix(in srgb, #3b82f6 10%, transparent); border-right: 1px solid var(--border-subtle); }
1329
+ .conflict-ours .conflict-side-label { color: #3b82f6; }
1330
+ .conflict-theirs { background: color-mix(in srgb, #f59e0b 10%, transparent); }
1331
+ .conflict-theirs .conflict-side-label { color: #f59e0b; }
1332
+
1333
+ .ws-file-clickable { cursor: pointer; border-radius: 4px; padding: 3px 4px; }
1334
+ .ws-file-clickable:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
1335
+
1336
+ /* Diff modal */
1337
+ .modal-wide { max-width: 720px; width: 90vw; }
1338
+ .diff-content { max-height: 60vh; overflow: auto; }
1339
+ .diff-header { font-size: 13px; font-weight: 600; color: var(--text-muted); padding: 8px 0 4px; }
1340
+ .diff-pre { margin: 0; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
1341
+ .diff-line { padding: 0 8px; }
1342
+ .diff-line-add { background: #dcfce7; color: #166534; }
1343
+ .diff-line-del { background: #fef2f2; color: #991b1b; }
1344
+ .diff-line-hunk { background: var(--surface); color: var(--accent); font-weight: 600; margin-top: 8px; }
1345
+ .diff-line-meta { color: var(--text-muted); font-weight: 600; }
1346
+ @media (prefers-color-scheme: dark) {
1347
+ .diff-line-add { background: #052e16; color: #86efac; }
1348
+ .diff-line-del { background: #450a0a; color: #fca5a5; }
1349
+ }
1126
1350
 
1127
1351
  .ws-action-item {
1128
1352
  font-size: 13px;
@@ -2226,17 +2450,82 @@ details.ws-commit-item[open] .ws-commit-arrow { transform: rotate(90deg); }
2226
2450
  .ws-commit-cat-item { color: var(--text-muted); padding-left: 12px; position: relative; line-height: 1.5; }
2227
2451
  .ws-commit-cat-item::before { content: '•'; position: absolute; left: 3px; color: var(--text-muted); }
2228
2452
 
2453
+ /* Preview toolbar — URL bar + viewport switcher */
2454
+ .ws-preview-toolbar {
2455
+ display: flex;
2456
+ align-items: center;
2457
+ gap: 8px;
2458
+ padding: 4px 8px;
2459
+ background: var(--bg);
2460
+ border-bottom: 1px solid var(--border-subtle);
2461
+ flex-shrink: 0;
2462
+ }
2463
+ .ws-url-bar {
2464
+ display: flex;
2465
+ align-items: center;
2466
+ gap: 4px;
2467
+ flex: 1;
2468
+ min-width: 0;
2469
+ }
2470
+ .ws-url-back, .ws-url-refresh {
2471
+ background: none;
2472
+ border: 1px solid var(--border-subtle);
2473
+ border-radius: 4px;
2474
+ width: 28px;
2475
+ height: 28px;
2476
+ cursor: pointer;
2477
+ font-size: 14px;
2478
+ color: var(--text-muted);
2479
+ display: flex;
2480
+ align-items: center;
2481
+ justify-content: center;
2482
+ flex-shrink: 0;
2483
+ }
2484
+ .ws-url-back:hover, .ws-url-refresh:hover { background: var(--surface); color: var(--text); }
2485
+ .ws-url-input {
2486
+ flex: 1;
2487
+ min-width: 0;
2488
+ height: 28px;
2489
+ padding: 0 8px;
2490
+ border: 1px solid var(--border-subtle);
2491
+ border-radius: 4px;
2492
+ background: var(--surface);
2493
+ color: var(--text);
2494
+ font-family: var(--font-mono);
2495
+ font-size: 12px;
2496
+ }
2497
+ .ws-url-input:focus { outline: none; border-color: var(--accent); }
2498
+ .ws-viewport-switcher {
2499
+ display: flex;
2500
+ gap: 2px;
2501
+ flex-shrink: 0;
2502
+ }
2503
+ .ws-vp-btn {
2504
+ background: none;
2505
+ border: 1px solid transparent;
2506
+ border-radius: 4px;
2507
+ width: 28px;
2508
+ height: 28px;
2509
+ cursor: pointer;
2510
+ font-size: 14px;
2511
+ display: flex;
2512
+ align-items: center;
2513
+ justify-content: center;
2514
+ }
2515
+ .ws-vp-btn:hover { background: var(--surface); }
2516
+ .ws-vp-btn.ws-vp-active { background: var(--surface); border-color: var(--accent); }
2517
+
2229
2518
  /* Split-view compare */
2230
2519
  .ws-preview-container { position: relative; flex: 1; display: flex; }
2231
2520
  .ws-preview-container .ws-preview-full { flex: 1; }
2232
2521
  .ws-preview-main { display: none; flex: 1; flex-direction: column; border-left: 2px solid var(--accent); position: relative; }
2233
2522
  .ws-preview-main.hidden { display: none !important; }
2234
2523
  .ws-split-view .ws-preview-full,
2235
- .ws-split-view .ws-preview-main { flex: 1; display: flex; flex-direction: column; }
2524
+ .ws-split-view .ws-preview-main { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative !important; top: auto !important; left: auto !important; right: auto !important; bottom: auto !important; }
2525
+ .ws-split-view .ws-preview-iframe { width: 100%; height: 100%; border: none; }
2236
2526
  .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
2527
  .ws-preview-loading { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-muted); font-size: 14px; }
2238
2528
  .btn-active { background: var(--accent) !important; color: white !important; }
2239
- .ws-split-view .ws-preview-full { position: relative; }
2240
2529
  .ws-split-view .ws-preview-full::before {
2241
2530
  content: '⛺ 내 캠프';
2242
2531
  position: absolute; top: 8px; left: 8px; z-index: 10;