axmap-cli 0.0.1

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.
Files changed (78) hide show
  1. package/.claude/commands/ax-done.md +10 -0
  2. package/.claude/commands/ax-setup.md +31 -0
  3. package/.claude/commands/ax-start.md +15 -0
  4. package/.claude/commands/ax-tell.md +14 -0
  5. package/.claude/commands/ax-update.md +19 -0
  6. package/.claude/commands/ax.md +13 -0
  7. package/CLAUDE.md +309 -0
  8. package/LICENSE +20 -0
  9. package/README.md +207 -0
  10. package/app/README.md +366 -0
  11. package/app/eval/edges.mjs +242 -0
  12. package/app/lib/adjacent.mjs +125 -0
  13. package/app/lib/agentcli.mjs +153 -0
  14. package/app/lib/analyze.mjs +1159 -0
  15. package/app/lib/cochange.mjs +421 -0
  16. package/app/lib/datanodes.mjs +127 -0
  17. package/app/lib/entry.mjs +192 -0
  18. package/app/lib/featuregraph.mjs +389 -0
  19. package/app/lib/features.mjs +645 -0
  20. package/app/lib/fetchrepo-run.mjs +37 -0
  21. package/app/lib/fetchrepo.mjs +164 -0
  22. package/app/lib/flow.mjs +1089 -0
  23. package/app/lib/ladder.mjs +387 -0
  24. package/app/lib/langs.mjs +630 -0
  25. package/app/lib/live.mjs +346 -0
  26. package/app/lib/llm.mjs +594 -0
  27. package/app/lib/newfile.mjs +126 -0
  28. package/app/lib/prdiff.mjs +651 -0
  29. package/app/lib/reveal.mjs +316 -0
  30. package/app/lib/roots.mjs +186 -0
  31. package/app/lib/scope.mjs +342 -0
  32. package/app/lib/session.mjs +389 -0
  33. package/app/lib/slots.mjs +233 -0
  34. package/app/lib/ssot.mjs +277 -0
  35. package/app/lib/teamview.mjs +962 -0
  36. package/app/lib/terms.ko.mjs +169 -0
  37. package/app/server.mjs +1959 -0
  38. package/app/web/shell.css +538 -0
  39. package/app/web/shell.html +197 -0
  40. package/app/web/shell.js +638 -0
  41. package/app/web/stage.js +347 -0
  42. package/app/web/words.js +85 -0
  43. package/bin/axmap.mjs +1918 -0
  44. package/governance/GOVERNANCE.md +433 -0
  45. package/governance/gate.mjs +526 -0
  46. package/governance/vote.mjs +501 -0
  47. package/mcp/README.md +254 -0
  48. package/mcp/SETUP-FOR-AI.md +186 -0
  49. package/mcp/install.ps1 +341 -0
  50. package/mcp/install.sh +339 -0
  51. package/mcp/server.mjs +969 -0
  52. package/package.json +48 -0
  53. package/src/closure.mjs +343 -0
  54. package/src/governance.mjs +839 -0
  55. package/src/invariants.mjs +226 -0
  56. package/src/mrtarget.mjs +284 -0
  57. package/src/promote.mjs +177 -0
  58. package/src/protocol.mjs +423 -0
  59. package/src/repotarget.mjs +81 -0
  60. package/src/update.mjs +177 -0
  61. package/src/version.mjs +186 -0
  62. package/tools/bus.mjs +520 -0
  63. package/tools/cluster-experiment.mjs +256 -0
  64. package/tools/cluster-sweep.mjs +226 -0
  65. package/tools/make-icon.mjs +108 -0
  66. package/tools/mcp-register.mjs +269 -0
  67. package/tools/mr-target.mjs +49 -0
  68. package/tools/persona-bench.mjs +362 -0
  69. package/tools/pick-repo.mjs +229 -0
  70. package/tools/promote.mjs +550 -0
  71. package/tools/reveal-demo.mjs +158 -0
  72. package/tools/run-tests.mjs +42 -0
  73. package/tools/setup.mjs +490 -0
  74. package/tools/shortcut.mjs +121 -0
  75. package/tools/smoke.mjs +166 -0
  76. package/tools/topicgraph.py +154 -0
  77. package/tools/vendor.mjs +382 -0
  78. package/tools/version.mjs +115 -0
@@ -0,0 +1,538 @@
1
+ /*
2
+ 새 껍데기의 스타일.
3
+
4
+ 🔴 `style.css` 와 **따로 둔다.** 그쪽은 기존 화면(`/classic`)의 것이고
5
+ 검은 배경에 밀도 높은 목록을 전제로 짜여 있다. 한 파일에 두 방향을 담으면
6
+ 한쪽을 고칠 때마다 다른 쪽이 깨진다.
7
+
8
+ 이 화면의 방향은 하나다 — **여백**. 기능을 덜어낸 자리를 다시 채우지 않는다.
9
+ */
10
+
11
+ /**
12
+ * 색과 치수는 Claude 브랜드 토큰에서 가져왔다.
13
+ * 출처: github.com/ricocc/brands-design-md · brands/claude/variables.css
14
+ *
15
+ * 눈으로 고른 값이 아니라 **출처가 있는 값**이라는 것이 요점이다.
16
+ * 고칠 일이 생기면 여기서 고치고, 쓰는 쪽은 토큰 이름만 쓴다 —
17
+ * 하드코딩된 색이 한 곳이라도 남으면 다음에 바꿀 때 그것만 안 바뀐다.
18
+ */
19
+ :root {
20
+ --bg: #faf9f5; /* canvas */
21
+ --side: #f5f0e8; /* surface-soft */
22
+ --card: #efe9de; /* surface-card */
23
+ --ink: #141413; /* ink — 제목 */
24
+ --ink2: #6c6a64; /* muted */
25
+ --ink3: #8e8b82; /* muted-soft */
26
+ --line: #e6dfd8; /* hairline */
27
+ --hover: #efe9de;
28
+ --accent: #cc785c; /* primary */
29
+ --accent-down: #a9583e; /* primary-active */
30
+ --accent-tint: #f7ede8; /* primary 를 canvas 위에 옅게 깐 것 */
31
+ --warn: #c64545; /* error */
32
+ --on-accent: #ffffff; /* on-primary — 주황 위에 얹는 글자 */
33
+
34
+ --r-sm: 6px;
35
+ --r-md: 8px;
36
+ --r-lg: 12px;
37
+ --r-xl: 16px;
38
+
39
+ --mono: "JetBrains Mono", "Cascadia Mono", "D2Coding", Consolas, monospace;
40
+ /**
41
+ * 🔴 브랜드의 표시용 세리프(Copernicus · Tiempos Headline)는 **안 쓴다.**
42
+ *
43
+ * 이 화면은 한국어가 먼저다. 그 서체들은 한글 글리프가 없어서 제목의 한글만
44
+ * 시스템 서체로 떨어지고, 그러면 한 줄 안에서 라틴과 한글의 인상이 갈린다.
45
+ * 색은 브랜드를 따르되 서체는 한글이 읽히는 쪽을 고른다.
46
+ */
47
+ --ui: "Pretendard", "StyreneB", Inter, -apple-system, "Segoe UI", system-ui, sans-serif;
48
+ }
49
+
50
+ * { box-sizing: border-box; }
51
+
52
+ /**
53
+ * 🔴 hidden 속성이 항상 이기게 한다.
54
+ *
55
+ * hidden 은 UA 스타일시트의 display:none 으로 동작하는데, 우리가 그 요소에
56
+ * display:flex 를 직접 주면 **작성자 스타일이 이겨서 안 숨는다.**
57
+ *
58
+ * 실제로 #onboard 가 그랬다 — hidden = true 를 줘도 온보딩이 화면을 덮은 채
59
+ * 남아 있었고, 자바스크립트는 아무 오류도 내지 않았다. 눌러 보기 전에는
60
+ * 알 방법이 없는 종류의 실패다.
61
+ */
62
+ [hidden] { display: none !important; }
63
+
64
+ html, body {
65
+ height: 100%; margin: 0;
66
+ background: var(--bg); color: var(--ink);
67
+ font: 14px/1.6 var(--ui);
68
+ -webkit-font-smoothing: antialiased;
69
+ }
70
+
71
+ button { font: inherit; cursor: pointer; }
72
+
73
+ #app { display: flex; height: 100%; }
74
+
75
+ /* 분할선 — 평소에는 보이지 않고 잡을 때만 드러난다 */
76
+ .gut { width: 1px; flex: none; background: var(--line); cursor: col-resize; position: relative; }
77
+ .gut::after { content: ''; position: absolute; inset: 0 -3px; }
78
+ .gut:hover { background: var(--ink3); }
79
+
80
+ /* ── 왼쪽 ──────────────────────────────────────────────────────────────── */
81
+
82
+ #side {
83
+ width: 260px; min-width: 200px; flex: none;
84
+ background: var(--side);
85
+ display: flex; flex-direction: column;
86
+ padding: 10px;
87
+ }
88
+
89
+ #tabs { display: flex; gap: 4px; margin-bottom: 14px; }
90
+ .tab {
91
+ flex: 1; padding: 7px 0; border: 0; border-radius: 8px;
92
+ background: transparent; color: var(--ink2); font-size: 13px;
93
+ }
94
+ .tab:hover { background: var(--hover); color: var(--ink); }
95
+ .tab.on { background: var(--bg); color: var(--ink); box-shadow: 0 1px 2px rgba(0,0,0,.06); }
96
+
97
+ #new {
98
+ display: flex; align-items: center; gap: 10px; width: 100%;
99
+ padding: 8px 10px; margin-bottom: 22px;
100
+ border: 0; border-radius: 8px; background: transparent;
101
+ color: var(--ink); font-size: 14px; text-align: left;
102
+ }
103
+ #new:hover { background: var(--hover); }
104
+ #new span { flex: 1; }
105
+
106
+ .sec {
107
+ font-size: 12px; color: var(--ink3);
108
+ padding: 0 10px 8px;
109
+ }
110
+
111
+ #chats { flex: 1; overflow: auto; margin: 0 -4px; }
112
+ .chat {
113
+ display: flex; align-items: center; gap: 9px;
114
+ padding: 7px 10px; border-radius: 8px;
115
+ font-size: 13px; color: var(--ink2);
116
+ overflow: hidden; white-space: nowrap;
117
+ }
118
+ .chat:hover { background: var(--hover); color: var(--ink); }
119
+ .chat.on { background: var(--hover); color: var(--ink); }
120
+ .chat .dot {
121
+ width: 5px; height: 5px; border-radius: 50%; flex: none;
122
+ border: 1px solid var(--ink3);
123
+ }
124
+ .chat.run .dot { background: var(--accent); border-color: var(--accent); }
125
+ .chat.err .dot { background: var(--warn); border-color: var(--warn); }
126
+ .chat .name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
127
+ .chat .n { font: 10px var(--mono); color: var(--accent); flex: none; }
128
+ .empty { padding: 4px 10px; font-size: 12px; color: var(--ink3); }
129
+
130
+ #foot {
131
+ border-top: 1px solid var(--line);
132
+ padding: 10px 10px 2px; margin-top: 8px;
133
+ display: flex; flex-direction: column; gap: 2px;
134
+ font-size: 11px; color: var(--ink3);
135
+ }
136
+ #foot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
137
+
138
+ /* ── 가운데 ────────────────────────────────────────────────────────────── */
139
+
140
+ /*
141
+ 🔴 지금은 비어 있다. 그래프가 들어올 자리다.
142
+
143
+ 기존 화면의 캔버스를 그대로 옮겨 오지 않은 이유: 그 렌더러는 검은 배경을
144
+ 전제로 색을 고른다(`graph.js` 의 명암·안개). 밝은 바탕에 그대로 얹으면
145
+ 읽히지 않는다. 옮길 때 색 채널을 다시 정하고 옮긴다.
146
+ */
147
+ #center { flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; }
148
+ #stage { color: var(--ink3); font-size: 12px; }
149
+
150
+ /* ── 오른쪽 · 대화 ─────────────────────────────────────────────────────── */
151
+
152
+ #chat {
153
+ width: 520px; min-width: 360px; flex: none;
154
+ display: flex; flex-direction: column; min-height: 0;
155
+ }
156
+
157
+ #head {
158
+ flex: none; display: flex; align-items: center; gap: 10px;
159
+ padding: 14px 22px 6px; font-size: 13px; color: var(--ink2);
160
+ min-height: 42px;
161
+ }
162
+ #title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
163
+ #stop {
164
+ border: 1px solid var(--line); background: var(--bg); color: var(--ink2);
165
+ border-radius: 7px; padding: 3px 10px; font-size: 12px;
166
+ }
167
+ #stop:hover { color: var(--warn); border-color: var(--warn); }
168
+
169
+ #touched {
170
+ flex: none; margin: 0 22px 10px; padding: 9px 12px;
171
+ border: 1px solid var(--line); border-radius: 10px;
172
+ font-size: 12px; color: var(--ink2); background: var(--bg);
173
+ }
174
+ #touched b { color: var(--ink); font-weight: 600; }
175
+ #touched .f {
176
+ display: block; font: 11px var(--mono); color: var(--ink);
177
+ padding-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
178
+ }
179
+
180
+ #thread {
181
+ flex: 1; overflow: auto; padding: 6px 22px 10px;
182
+ display: flex; flex-direction: column; gap: 16px;
183
+ }
184
+ #thread:empty { flex: 0; padding: 0; }
185
+
186
+ /* 대화가 없을 때 — 인사말과 입력창이 칸 가운데에 뜬다 */
187
+ #tail { flex: 0; }
188
+ #chat.blank #tail { flex: 1; }
189
+
190
+ /* 대화가 없을 때의 인사. 스크린샷의 가운데 그 화면이다. */
191
+ #hello {
192
+ flex: 1; display: flex; flex-direction: column;
193
+ align-items: center; justify-content: flex-end;
194
+ padding-bottom: 26px; gap: 14px;
195
+ }
196
+ #hello .mark { color: var(--accent); font-size: 26px; line-height: 1; }
197
+ #hello h1 { margin: 0; font-size: 28px; font-weight: 400; letter-spacing: -.01em; }
198
+
199
+ .msg { font-size: 14px; line-height: 1.7; white-space: pre-wrap; word-break: break-word; }
200
+ .msg.user {
201
+ align-self: flex-end; max-width: 85%;
202
+ background: var(--side); padding: 10px 15px; border-radius: 16px 16px 4px 16px;
203
+ }
204
+ .msg.assistant { align-self: flex-start; }
205
+ .msg.error {
206
+ align-self: flex-start; color: var(--warn); font-size: 13px;
207
+ border-left: 2px solid var(--warn); padding-left: 10px;
208
+ }
209
+ .msg.note { align-self: center; color: var(--ink3); font-size: 12px; }
210
+
211
+ /* 도구 한 줄 — 대화가 아니라 **행동**이라 다르게 그린다 */
212
+ .msg.tool {
213
+ align-self: flex-start; display: flex; gap: 8px; align-items: baseline;
214
+ font: 11px/1.5 var(--mono); color: var(--ink3); max-width: 100%;
215
+ }
216
+ .msg.tool .t { flex: none; }
217
+ .msg.tool .a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
218
+ .msg.tool.edits .t { color: var(--accent); }
219
+ .msg.tool.edits .a { color: var(--ink); }
220
+
221
+ /* 계측·잡음은 버리지 않고 접어 둔다 */
222
+ details.msg { align-self: flex-start; max-width: 100%; }
223
+ details.msg summary { font-size: 11px; color: var(--ink3); cursor: pointer; list-style: none; }
224
+ details.msg pre {
225
+ font: 11px var(--mono); color: var(--ink2); white-space: pre-wrap;
226
+ word-break: break-all; margin: 6px 0 0;
227
+ }
228
+
229
+ .think { align-self: flex-start; color: var(--ink3); font-size: 13px; }
230
+ .think::after { content: ''; animation: d 1.4s steps(4, end) infinite; }
231
+ @keyframes d { 0% { content: '' } 25% { content: '.' } 50% { content: '..' } 75% { content: '...' } }
232
+
233
+ /* ── 입력 ──────────────────────────────────────────────────────────────── */
234
+
235
+ #composer {
236
+ flex: none; margin: 0 22px 22px; padding: 14px 14px 10px;
237
+ border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--bg);
238
+ box-shadow: 0 1px 3px rgba(0,0,0,.04);
239
+ }
240
+ #composer:focus-within { border-color: var(--ink3); }
241
+ #input {
242
+ width: 100%; border: 0; outline: none; resize: none; background: transparent;
243
+ font: 14px/1.6 var(--ui); color: var(--ink); max-height: 200px;
244
+ }
245
+ #input::placeholder { color: var(--ink3); }
246
+ #composer .row { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
247
+ #note { flex: 1; font-size: 11px; color: var(--ink3); }
248
+ #send {
249
+ flex: none; width: 30px; height: 30px; border-radius: 9px; border: 0;
250
+ background: var(--accent); color: var(--on-accent); font-size: 14px;
251
+ }
252
+ #send:hover { background: var(--accent-down); }
253
+ #send:disabled { background: var(--line); color: var(--ink3); cursor: default; }
254
+
255
+ /* ══ 창 테두리 = 툴바 ═══════════════════════════════════════════════════════
256
+
257
+ 🔴 이 줄은 **새로 만든 줄이 아니라 창의 제목표시줄**이다 (`frame: false`).
258
+
259
+ 그래서 높이를 32px 로 낮추고 아래 테두리를 없앴다. 사이드바와 같은 색이라
260
+ 왼쪽 위 모서리가 한 덩어리로 이어져 보인다 — 띠가 하나 더 얹힌 것처럼
261
+ 보이면 그 줄은 낭비다.
262
+
263
+ 빈 곳은 끌어서 창을 옮길 수 있어야 하고(`app-region: drag`), 버튼은 반대로
264
+ 끌기에서 빼야 한다 — 안 빼면 눌리지 않는다. 실제로 그렇게 안 눌렸다.
265
+ ────────────────────────────────────────────────────────────────────────── */
266
+
267
+ body { display: flex; flex-direction: column; }
268
+
269
+ #bar {
270
+ flex: none; display: flex; align-items: center; gap: 2px;
271
+ height: 32px; padding-left: 6px;
272
+ background: var(--side);
273
+ -webkit-app-region: drag;
274
+ user-select: none;
275
+ }
276
+
277
+ /* 아이콘 단추 — 인라인 SVG. 아이콘 글꼴도 이미지도 안 쓴다 */
278
+ .ico {
279
+ -webkit-app-region: no-drag;
280
+ display: grid; place-items: center;
281
+ width: 30px; height: 26px; padding: 0;
282
+ border: 0; border-radius: var(--r-sm); background: transparent;
283
+ color: var(--ink2);
284
+ }
285
+ .ico:hover, .ico.open { background: var(--hover); color: var(--ink); }
286
+ .ico svg {
287
+ width: 16px; height: 16px;
288
+ fill: none; stroke: currentColor; stroke-width: 1.4;
289
+ stroke-linecap: round; stroke-linejoin: round;
290
+ }
291
+ /* 폴더는 채워야 폴더로 읽힌다 */
292
+ #openrepo svg { fill: currentColor; fill-opacity: .13; }
293
+
294
+ /*
295
+ 지금 보고 있는 저장소. 창 제목이 놓이는 자리이고, **글자로 남긴 유일한 것**이다.
296
+ VS Code 가 여기에 파일 이름을 두는 것과 같은 이유 — 창 제목은 "무엇을 보고 있나" 다.
297
+ */
298
+ #where {
299
+ flex: 1; text-align: center; font-size: 11px; color: var(--ink3);
300
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
301
+ padding: 0 12px;
302
+ }
303
+
304
+ #wctl { display: flex; -webkit-app-region: no-drag; }
305
+ #wctl button {
306
+ display: grid; place-items: center;
307
+ width: 44px; height: 32px; border: 0; background: transparent; color: var(--ink2);
308
+ }
309
+ #wctl svg {
310
+ width: 12px; height: 12px;
311
+ fill: none; stroke: currentColor; stroke-width: 1.1; stroke-linecap: round;
312
+ }
313
+ #wctl button:hover { background: var(--hover); color: var(--ink); }
314
+ #wctl button[data-win="close"]:hover { background: var(--warn); color: var(--on-accent); }
315
+
316
+ /* 메뉴 펼침 */
317
+ #drop {
318
+ position: fixed; z-index: 40; min-width: 220px; padding: 5px;
319
+ background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-lg);
320
+ box-shadow: 0 8px 24px rgba(0,0,0,.12);
321
+ }
322
+ #drop button {
323
+ display: flex; width: 100%; align-items: center; gap: 10px;
324
+ border: 0; background: transparent; color: var(--ink);
325
+ font-size: 13px; padding: 7px 10px; border-radius: var(--r-sm); text-align: left;
326
+ }
327
+ #drop button:hover { background: var(--hover); }
328
+ #drop button:disabled { color: var(--ink3); }
329
+ #drop .k { margin-left: auto; font: 10px var(--mono); color: var(--ink3); }
330
+ #drop hr { border: 0; border-top: 1px solid var(--line); margin: 5px 2px; }
331
+
332
+ #app { flex: 1; min-height: 0; }
333
+
334
+ /* 사이드바 접기 — 가운데 칸에 자리를 준다 */
335
+ body.no-side #side,
336
+ body.no-side .gut[data-pane="side"] { display: none; }
337
+
338
+ /* ══ 처음 오는 사람 ═════════════════════════════════════════════════════════ */
339
+
340
+ #onboard {
341
+ position: fixed; inset: 0; z-index: 60;
342
+ background: var(--bg);
343
+ display: flex; align-items: center; justify-content: center;
344
+ padding: 24px;
345
+ }
346
+ #onboard .card { width: 100%; max-width: 460px; text-align: center; }
347
+ #onboard .mark { color: var(--accent); font-size: 26px; margin-bottom: 12px; }
348
+ #onboard h2 { margin: 0 0 8px; font-size: 24px; font-weight: 400; }
349
+ #onboard p { margin: 0 0 22px; font-size: 13px; color: var(--ink2); line-height: 1.8; }
350
+
351
+ .steps { display: flex; gap: 6px; justify-content: center; margin-bottom: 26px; }
352
+ .steps i { width: 22px; height: 3px; border-radius: 2px; background: var(--line); }
353
+ .steps i.on { background: var(--accent); }
354
+
355
+ /* CLI 고르기 — 설치·로그인 상태를 카드에 그대로 적는다 */
356
+ #picks { display: flex; flex-direction: column; gap: 8px; margin-bottom: 22px; }
357
+ .pick {
358
+ display: flex; align-items: center; gap: 12px; width: 100%;
359
+ padding: 13px 15px; border: 1px solid var(--line); border-radius: 12px;
360
+ background: var(--bg); text-align: left;
361
+ }
362
+ .pick:hover:not(:disabled) { border-color: var(--ink3); }
363
+ .pick.on { border-color: var(--accent); background: var(--accent-tint); }
364
+ .pick:disabled { opacity: .5; cursor: default; }
365
+ .pick .nm { flex: 1; font-size: 14px; color: var(--ink); }
366
+ .pick .st { font-size: 11px; color: var(--ink3); }
367
+ .pick .st.ok { color: var(--accent); }
368
+ .pick .st.warn { color: #a8742a; }
369
+
370
+ .open { display: flex; gap: 8px; margin-bottom: 12px; }
371
+ #url {
372
+ flex: 1; padding: 11px 14px; border: 1px solid var(--line); border-radius: var(--r-lg);
373
+ font: 13px var(--ui); color: var(--ink); outline: none; background: var(--bg);
374
+ }
375
+ #url:focus { border-color: var(--ink3); }
376
+ #go, #s1next {
377
+ padding: 11px 20px; border: 0; border-radius: 11px;
378
+ background: var(--accent); color: var(--on-accent); font-size: 13px;
379
+ }
380
+ #go:disabled, #s1next:disabled { background: var(--line); color: var(--ink3); cursor: default; }
381
+ #pick {
382
+ width: 100%; padding: 11px; margin-bottom: 12px;
383
+ border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--bg);
384
+ color: var(--ink2); font-size: 13px;
385
+ }
386
+ #pick:hover { border-color: var(--ink3); color: var(--ink); }
387
+
388
+ #omsg { min-height: 20px; font-size: 12px; color: var(--ink2); margin-bottom: 14px; }
389
+ #omsg.err { color: var(--warn); }
390
+
391
+ .act { display: flex; gap: 8px; justify-content: center; align-items: center; }
392
+ .ghost {
393
+ border: 0; background: transparent; color: var(--ink3);
394
+ font-size: 12px; padding: 8px 10px; border-radius: 8px;
395
+ }
396
+ .ghost:hover { background: var(--hover); color: var(--ink2); }
397
+
398
+ /* ══ 모달 ═══════════════════════════════════════════════════════════════════
399
+ 처음 오는 사람에게는 화면 전체를 쓰고, 그 뒤 `저장소 열기` 로 부를 때는
400
+ 모달로 뜬다. 같은 markup 을 쓰되 겉모습만 바꾼다 — 두 벌을 만들면
401
+ 한쪽만 고쳐서 어긋난다.
402
+ ────────────────────────────────────────────────────────────────────────── */
403
+
404
+ #onboard.as-modal { background: rgba(20, 20, 19, .34); }
405
+ #onboard.as-modal .card {
406
+ background: var(--bg);
407
+ border: 1px solid var(--line);
408
+ border-radius: var(--r-xl);
409
+ box-shadow: 0 18px 48px rgba(0, 0, 0, .18);
410
+ padding: 34px 34px 26px;
411
+ max-width: 480px;
412
+ }
413
+ /* 모달일 때는 걸음 표시가 뜻이 없다 — 한 걸음짜리다 */
414
+ #onboard.as-modal .steps { display: none; }
415
+
416
+ #oclose {
417
+ position: absolute; top: 14px; right: 16px;
418
+ border: 0; background: transparent; color: var(--ink3);
419
+ font-size: 15px; line-height: 1; padding: 6px 8px; border-radius: var(--r-sm);
420
+ }
421
+ #oclose:hover { background: var(--hover); color: var(--ink); }
422
+ #onboard:not(.as-modal) #oclose { display: none; }
423
+ #onboard .card { position: relative; }
424
+
425
+ /* ══ 가운데 칸 — ladder 를 번호 카드로 ══════════════════════════════════════
426
+ 조판 규율은 Zap 편집기에서 가져왔다. 실측값과 근거는 stage.js 머리말과
427
+ docs/RESEARCH-FLOWCANVAS.md 1.1 에 있다.
428
+ ────────────────────────────────────────────────────────────────────────── */
429
+
430
+ #center { flex: 1; min-width: 0; display: block; overflow: auto; }
431
+
432
+ #stage { position: relative; padding: 22px 26px 40px; min-height: 100%; }
433
+ #stage.loading, #stage.note {
434
+ display: flex; align-items: center; justify-content: center;
435
+ color: var(--ink3); font-size: 12px; text-align: center; padding: 40px;
436
+ }
437
+
438
+ /* 선은 카드 아래에 깔린다. 클릭을 막지 않는다. */
439
+ .wires { position: absolute; left: 0; top: 0; pointer-events: none; }
440
+ /*
441
+ 🔴 선이 조용해야 한다. 실측한 Zapier 연결선은 바탕 대비 4.4:1 밖에 안 된다 —
442
+ 주인공은 카드의 글자이고 선은 배경 구조물이라는 선언이다.
443
+ */
444
+ .w { fill: none; stroke: var(--line); stroke-width: 1.5; }
445
+
446
+ .board { position: relative; }
447
+
448
+ /* ── 겹 ─────────────────────────────────────────────────────────────────── */
449
+
450
+ .layer { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 34px; }
451
+
452
+ .lhead {
453
+ flex: none; width: 54px; padding-top: 4px; text-align: right;
454
+ display: flex; flex-direction: column; gap: 1px;
455
+ }
456
+ .lno { font-size: 12px; color: var(--ink); }
457
+ .lcount { font-size: 10px; color: var(--ink3); }
458
+
459
+ .cols { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: 12px 22px; align-items: flex-start; }
460
+
461
+ /* ── 묶음 ───────────────────────────────────────────────────────────────── */
462
+
463
+ .grp { display: flex; flex-direction: column; gap: 8px; }
464
+
465
+ /* 가지 머리의 알약 칩. Zap 편집기가 조건문을 두는 자리에 우리는 디렉터리를 둔다. */
466
+ .chip {
467
+ display: inline-flex; align-items: baseline; gap: 6px; align-self: flex-start;
468
+ padding: 3px 10px; border-radius: var(--r-pill, 999px);
469
+ border: 1px solid var(--line); background: var(--bg);
470
+ font: 11px var(--mono); color: var(--ink2);
471
+ max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
472
+ }
473
+ .chip .cn { color: var(--accent); font-size: 10px; }
474
+
475
+ /* ── 카드 ───────────────────────────────────────────────────────────────── */
476
+
477
+ /*
478
+ 폭을 고정한다. 실측한 Zapier 카드는 폭:높이 = 4.2:1 이고, 카드 사이 세로 여백이
479
+ 카드 높이보다 크다(1.24배). 그 비율이 이 화면이 빽빽하지 않게 느껴지는 이유다.
480
+ */
481
+ .card {
482
+ width: 210px; box-sizing: border-box;
483
+ padding: 8px 11px 9px;
484
+ border: 1px solid var(--line); border-radius: var(--r-md);
485
+ background: var(--bg);
486
+ position: relative; z-index: 1;
487
+ }
488
+ .card:hover { border-color: var(--ink3); }
489
+ .card.aux { opacity: .62; }
490
+
491
+ .card .r1 {
492
+ display: flex; gap: 8px; align-items: baseline;
493
+ font-size: 10px; color: var(--ink3); margin-bottom: 3px;
494
+ }
495
+ .card .r1 .opens { color: var(--accent); }
496
+ .card .r1 .lines { margin-left: auto; }
497
+ .card .r1 .tag { color: var(--ink3); }
498
+
499
+ .card .r2 { display: flex; gap: 7px; align-items: baseline; }
500
+ /* 🔴 번호가 이 카드의 정체다. 그림을 안 봐도 이 번호로 서로를 가리킬 수 있다. */
501
+ .card .r2 .no { font: 11px var(--mono); color: var(--ink3); flex: none; }
502
+ .card .r2 .nm {
503
+ font-size: 13px; color: var(--ink); min-width: 0;
504
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
505
+ }
506
+ .card .dir {
507
+ font: 10px var(--mono); color: var(--ink3); padding-top: 2px;
508
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
509
+ }
510
+
511
+ .more { font-size: 11px; color: var(--ink3); align-self: center; }
512
+
513
+ .foot2 {
514
+ display: flex; flex-wrap: wrap; gap: 14px;
515
+ padding-top: 8px; border-top: 1px solid var(--line);
516
+ font-size: 11px; color: var(--ink3);
517
+ }
518
+ /* 자른 것은 잘랐다고 말한다. 조용히 자르면 "이게 전부" 로 읽힌다. */
519
+ .foot2 .warn2 { color: var(--accent); }
520
+
521
+ /* 프로젝트 고르는 줄 — 모노레포에서 "어디서부터" 의 첫 답이다 */
522
+ .projs {
523
+ display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
524
+ margin-bottom: 22px; padding-bottom: 16px; border-bottom: 1px solid var(--line);
525
+ }
526
+ .proj {
527
+ display: inline-flex; align-items: baseline; gap: 7px;
528
+ padding: 5px 12px; border-radius: var(--r-pill, 999px);
529
+ border: 1px solid var(--line); background: var(--bg); color: var(--ink2);
530
+ font-size: 12px;
531
+ }
532
+ .proj:hover { border-color: var(--ink3); color: var(--ink); }
533
+ .proj.on { border-color: var(--accent); background: var(--accent-tint); color: var(--ink); }
534
+ .proj .pn { font-family: var(--mono); font-size: 11px; }
535
+ .proj .pf { color: var(--accent); font-size: 10px; }
536
+ /* 🔴 선언된 것과 짐작한 것을 눈으로 구분한다 (D5) */
537
+ .proj .guess { color: var(--ink3); font-size: 10px; }
538
+ .pout { font-size: 11px; color: var(--ink3); margin-left: 4px; }