bothouse-dsh 0.1.45 → 0.1.47

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/collab/page.html CHANGED
@@ -2,60 +2,511 @@
2
2
  <html lang="zh-CN">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <meta name="color-scheme" content="dark light">
6
7
  <title>CoNest 频道</title>
7
8
  <style>
8
- :root { --bg: #313338; --side: #2b2d31; --rail: #1e1f22; --text: #dbdee1; --muted: #949ba4; --line: #3f4147; --accent: #5865f2; --hover: #35373c; --input: #383a40; --ok: #23a55a; }
9
+ /* 设计令牌:颜色 / 字号 / 间距 / 圆角集中在这里,亮暗两套共用同一组名字。
10
+ 对比度按 WCAG AA 取(正文与次要文字 ≥ 4.5:1,大字与描边 ≥ 3:1)。 */
11
+ :root {
12
+ color-scheme: dark;
13
+ --bg: #313338;
14
+ --surface: #2b2d31;
15
+ --surface-2: #35373c;
16
+ --rail: #1e1f22;
17
+ --text: #e3e5e8;
18
+ --text-strong: #f2f3f5;
19
+ --muted: #a9b0ba;
20
+ --faint: #9ea5af;
21
+ --line: #3f4147;
22
+ --hover: #35373c;
23
+ --hover-soft: rgba(127, 127, 127, .07);
24
+ --active: #404249;
25
+ --input: #383a40;
26
+ --accent: #5865f2;
27
+ --accent-strong: #4752c4;
28
+ --on-accent: #ffffff;
29
+ --ok: #23a55a;
30
+ --warn: #f0b232;
31
+ --danger: #f23f43;
32
+ --scrim: rgba(0, 0, 0, .55);
33
+ --shadow: 0 12px 32px rgba(0, 0, 0, .35);
34
+
35
+ /* 字号 */
36
+ --fs-xs: 12px;
37
+ --fs-sm: 13px;
38
+ --fs-md: 15px;
39
+ --fs-lg: 17px;
40
+ --fs-xl: 20px;
41
+
42
+ /* 间距 */
43
+ --sp-1: 4px;
44
+ --sp-2: 8px;
45
+ --sp-3: 12px;
46
+ --sp-4: 16px;
47
+ --sp-5: 20px;
48
+ --sp-6: 24px;
49
+ --sp-8: 32px;
50
+
51
+ /* 圆角 */
52
+ --r-sm: 6px;
53
+ --r-md: 10px;
54
+ --r-lg: 14px;
55
+ --r-full: 999px;
56
+
57
+ /* 布局 */
58
+ --rail-w: 240px;
59
+ --members-w: 240px;
60
+ --topbar-h: 52px;
61
+ --pad-x: 16px;
62
+ --avatar: 40px;
63
+ --avatar-sm: 32px;
64
+
65
+ /* 动效 */
66
+ --dur: 160ms;
67
+ --ease: cubic-bezier(.2, .8, .2, 1);
68
+ }
69
+
9
70
  @media (prefers-color-scheme: light) {
10
- :root { --bg: #ffffff; --side: #f2f3f5; --rail: #e3e5e8; --text: #313338; --muted: #5c5e66; --line: #e1e2e4; --hover: #ebedef; --input: #ebedef; }
71
+ :root {
72
+ color-scheme: light;
73
+ --bg: #ffffff;
74
+ --surface: #f6f7f9;
75
+ --surface-2: #eceef1;
76
+ --rail: #e6e8eb;
77
+ --text: #2b2d31;
78
+ --text-strong: #111214;
79
+ --muted: #545861;
80
+ --faint: #656a73;
81
+ --line: #dfe1e5;
82
+ --hover: #e9ebee;
83
+ --hover-soft: rgba(17, 18, 20, .035);
84
+ --active: #dfe2e7;
85
+ --input: #eceef1;
86
+ --accent: #4a55d6;
87
+ --accent-strong: #3d48c0;
88
+ --on-accent: #ffffff;
89
+ --ok: #1a7f43;
90
+ --warn: #7a5200;
91
+ --danger: #c22b2f;
92
+ --scrim: rgba(16, 18, 22, .42);
93
+ --shadow: 0 12px 32px rgba(20, 22, 28, .16);
94
+ }
95
+ }
96
+
97
+ @media (prefers-reduced-motion: reduce) {
98
+ :root { --dur: 0ms; }
11
99
  }
100
+
12
101
  * { box-sizing: border-box; }
13
102
  html, body { height: 100%; margin: 0; }
14
- body { background: var(--bg); color: var(--text); font: 15px/1.45 system-ui, -apple-system, "PingFang SC", sans-serif; display: grid; grid-template-columns: 240px 1fr 240px; }
15
- aside { background: var(--side); overflow-y: auto; }
16
- .head { height: 48px; padding: 0 16px; display: flex; align-items: center; font-weight: 600; border-bottom: 1px solid var(--rail); }
17
- .rooms button { all: unset; display: block; margin: 2px 8px; padding: 6px 8px; border-radius: 4px; color: var(--muted); cursor: pointer; }
103
+ body {
104
+ height: 100dvh;
105
+ overflow: hidden;
106
+ display: grid;
107
+ grid-template-columns: var(--rail-w) minmax(0, 1fr) var(--members-w);
108
+ background: var(--bg);
109
+ color: var(--text);
110
+ font: var(--fs-md)/1.45 system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
111
+ -webkit-font-smoothing: antialiased;
112
+ text-rendering: optimizeLegibility;
113
+ }
114
+ button { font: inherit; color: inherit; }
115
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
116
+
117
+ /* ---------- 侧栏 ---------- */
118
+ aside {
119
+ min-height: 0;
120
+ overflow-y: auto;
121
+ overscroll-behavior: contain;
122
+ background: var(--surface);
123
+ display: flex;
124
+ flex-direction: column;
125
+ }
126
+ #roomsPane { border-right: 1px solid var(--rail); }
127
+ #members { border-left: 1px solid var(--rail); }
128
+ .pane-head {
129
+ position: sticky;
130
+ top: 0;
131
+ z-index: 2;
132
+ flex: none;
133
+ display: flex;
134
+ align-items: center;
135
+ gap: var(--sp-2);
136
+ height: var(--topbar-h);
137
+ padding: 0 var(--pad-x);
138
+ padding-left: max(var(--pad-x), env(safe-area-inset-left));
139
+ padding-right: max(var(--pad-x), env(safe-area-inset-right));
140
+ font-weight: 700;
141
+ font-size: var(--fs-lg);
142
+ letter-spacing: .01em;
143
+ color: var(--text-strong);
144
+ background: var(--surface);
145
+ border-bottom: 1px solid var(--rail);
146
+ }
147
+ .pane-head .brand {
148
+ width: 22px; height: 22px;
149
+ border-radius: var(--r-sm);
150
+ background: var(--accent);
151
+ color: var(--on-accent);
152
+ font-size: 12px; font-weight: 700;
153
+ display: grid; place-items: center;
154
+ flex: none;
155
+ }
156
+
157
+ .rooms { padding: var(--sp-2) var(--sp-2) var(--sp-4); display: flex; flex-direction: column; gap: 2px; }
158
+ .rooms button {
159
+ all: unset;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: var(--sp-2);
163
+ min-height: 34px;
164
+ padding: 6px var(--sp-2);
165
+ border-radius: var(--r-sm);
166
+ color: var(--muted);
167
+ font-size: var(--fs-md);
168
+ cursor: pointer;
169
+ white-space: nowrap;
170
+ overflow: hidden;
171
+ text-overflow: ellipsis;
172
+ transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
173
+ }
174
+ .rooms button .hash { color: var(--faint); font-weight: 600; flex: none; }
18
175
  .rooms button:hover { background: var(--hover); color: var(--text); }
19
- .rooms button[aria-current="true"] { background: var(--hover); color: var(--text); }
20
- main { display: flex; flex-direction: column; min-width: 0; }
21
- #log { flex: 1; overflow-y: auto; padding: 8px 0 16px; }
22
- .msg { display: grid; grid-template-columns: 40px 1fr; gap: 0 12px; padding: 4px 16px; }
23
- .msg:hover { background: rgba(127,127,127,.06); }
24
- .avatar { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; font-weight: 700; color: #fff; position: relative; }
25
- .avatar img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
26
- .who { font-weight: 600; }
27
- .when { color: var(--muted); font-size: 12px; margin-left: 8px; font-weight: 400; }
28
- .body { white-space: pre-wrap; word-break: break-word; }
29
- .tag { font-size: 10px; background: var(--accent); color: #fff; border-radius: 3px; padding: 0 4px; margin-left: 6px; vertical-align: 2px; }
176
+ .rooms button[aria-current="true"] { background: var(--active); color: var(--text-strong); font-weight: 600; }
177
+ .rooms button[aria-current="true"] .hash { color: var(--text); }
178
+
179
+ /* ---------- 主区 ---------- */
180
+ main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
181
+ .topbar {
182
+ flex: none;
183
+ display: flex;
184
+ align-items: center;
185
+ gap: var(--sp-2);
186
+ height: var(--topbar-h);
187
+ padding: 0 var(--pad-x);
188
+ padding-left: max(var(--pad-x), env(safe-area-inset-left));
189
+ padding-right: max(var(--pad-x), env(safe-area-inset-right));
190
+ border-bottom: 1px solid var(--line);
191
+ background: var(--bg);
192
+ position: relative;
193
+ z-index: 3;
194
+ }
195
+ .topbar .title {
196
+ margin: 0;
197
+ font-weight: 650;
198
+ font-size: var(--fs-xl);
199
+ line-height: 28px;
200
+ color: var(--text-strong);
201
+ white-space: nowrap;
202
+ overflow: hidden;
203
+ text-overflow: ellipsis;
204
+ }
205
+ .spacer { flex: 1; }
206
+ .icon-btn {
207
+ display: none;
208
+ place-items: center;
209
+ width: 36px; height: 36px;
210
+ flex: none;
211
+ padding: 0;
212
+ border: 1px solid var(--line);
213
+ border-radius: var(--r-sm);
214
+ background: var(--surface);
215
+ color: var(--text);
216
+ cursor: pointer;
217
+ }
218
+ .icon-btn:hover { background: var(--hover); }
219
+ .icon-btn svg { width: 18px; height: 18px; }
220
+ .mode {
221
+ border: 1px solid var(--line);
222
+ border-radius: var(--r-sm);
223
+ padding: 5px var(--sp-2);
224
+ background: var(--input);
225
+ color: var(--text);
226
+ font: var(--fs-sm)/1.2 system-ui, -apple-system, "PingFang SC", sans-serif;
227
+ max-width: 190px;
228
+ }
229
+ .mode:hover { background: var(--hover); }
230
+
231
+ #log { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: var(--sp-4) 0 var(--sp-6); scrollbar-gutter: stable; }
232
+
233
+ .msg {
234
+ position: relative;
235
+ display: grid;
236
+ grid-template-columns: var(--avatar) minmax(0, 1fr);
237
+ gap: 0 var(--sp-3);
238
+ padding: var(--sp-1) var(--pad-x);
239
+ padding-left: max(var(--pad-x), env(safe-area-inset-left));
240
+ padding-right: max(var(--pad-x), env(safe-area-inset-right));
241
+ border-radius: var(--r-sm);
242
+ transition: background var(--dur) var(--ease);
243
+ }
244
+ .msg:hover { background: var(--hover-soft); }
245
+ .msg.cont { padding-top: 2px; }
246
+ .msg.cont .avatar,
247
+ .msg.cont .head { display: none; }
248
+ .msg .stamp {
249
+ display: none;
250
+ position: absolute;
251
+ left: max(var(--pad-x), env(safe-area-inset-left));
252
+ top: 2px;
253
+ width: var(--avatar);
254
+ text-align: center;
255
+ font-size: var(--fs-xs);
256
+ line-height: 1.4;
257
+ color: var(--faint);
258
+ font-variant-numeric: tabular-nums;
259
+ }
260
+ .msg.cont:hover .stamp { display: block; }
261
+
262
+ .avatar {
263
+ width: var(--avatar);
264
+ height: var(--avatar);
265
+ border-radius: var(--r-full);
266
+ display: grid;
267
+ place-items: center;
268
+ font-weight: 700;
269
+ font-size: var(--fs-md);
270
+ color: #fff;
271
+ position: relative;
272
+ flex: none;
273
+ user-select: none;
274
+ }
275
+ .avatar img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: var(--r-full); object-fit: cover; }
276
+
277
+ .head { display: flex; align-items: baseline; gap: var(--sp-2); min-width: 0; }
278
+ .who { font-weight: 600; font-size: var(--fs-md); color: var(--text-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
279
+ .when { color: var(--faint); font-size: var(--fs-xs); white-space: nowrap; flex: none; font-variant-numeric: tabular-nums; }
280
+ .body { white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; color: var(--text); }
281
+ .tag {
282
+ font-size: 11px;
283
+ line-height: 1.6;
284
+ background: var(--accent);
285
+ color: var(--on-accent);
286
+ border-radius: 4px;
287
+ padding: 0 5px;
288
+ vertical-align: 1px;
289
+ flex: none;
290
+ }
30
291
  .system .body { color: var(--muted); font-style: italic; }
31
- .card { border-left: 4px solid var(--accent); background: var(--side); padding: 6px 10px; border-radius: 4px; margin-top: 2px; }
32
- .card.completed { border-color: var(--ok); } .card.failed { border-color: #f23f43; }
33
- .card .title { font-weight: 600; font-size: 13px; }
34
- .card .actions { display: flex; gap: 6px; margin-top: 6px; } .card .actions button { padding: 3px 10px; border-radius: 4px; border: 1px solid #ccc; cursor: pointer; }
35
- form { padding: 0 16px 20px; }
36
- textarea { width: 100%; resize: none; border: 0; border-radius: 8px; padding: 11px 14px; background: var(--input); color: var(--text); font: inherit; max-height: 40vh; }
37
- textarea:focus { outline: 2px solid var(--accent); }
38
- .members h3 { margin: 16px 16px 4px; font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .02em; }
39
- .member { display: flex; align-items: center; gap: 10px; padding: 5px 16px; }
40
- .member .avatar { width: 32px; height: 32px; font-size: 13px; position: relative; }
41
- .dot { position: absolute; right: -2px; bottom: -2px; width: 12px; height: 12px; border-radius: 50%; border: 3px solid var(--side); background: var(--ok); }
42
- .offline { opacity: .45; } .offline .dot { background: var(--muted); }
43
- .status { font-size: 12px; color: var(--muted); }
44
- .mode { margin-left: auto; border: 1px solid var(--line); border-radius: 4px; padding: 4px 8px; background: var(--input); color: var(--text); font: inherit; }
45
- #thinking { min-height: 20px; padding: 0 16px 4px; color: var(--muted); font-size: 13px; }
46
- @media (max-width: 760px) { body { grid-template-columns: 1fr; } aside { display: none; } }
292
+ .system .who { color: var(--muted); font-weight: 500; }
293
+
294
+ /* 任务卡:左边框 + 状态胶囊,五种状态各有颜色(暗/亮两套都单独取过对比度) */
295
+ .card {
296
+ --card-accent: var(--accent);
297
+ --card-tint: rgba(88, 101, 242, .16);
298
+ --card-text: #b7bdff;
299
+ border-left: 3px solid var(--card-accent);
300
+ background: var(--surface);
301
+ padding: var(--sp-2) var(--sp-3);
302
+ border-radius: var(--r-sm);
303
+ margin-top: var(--sp-1);
304
+ max-width: 640px;
305
+ }
306
+ .card[data-state="running"] { --card-accent: var(--warn); --card-tint: rgba(240, 178, 50, .16); --card-text: #f6d27a; }
307
+ .card[data-state="completed"] { --card-accent: var(--ok); --card-tint: rgba(35, 165, 90, .16); --card-text: #7ee2a8; }
308
+ .card[data-state="failed"] { --card-accent: var(--danger); --card-tint: rgba(242, 63, 67, .16); --card-text: #ff9a9c; }
309
+ .card[data-state="cancelled"] { --card-accent: var(--muted); --card-tint: rgba(127, 127, 127, .14); --card-text: var(--muted); }
310
+ @media (prefers-color-scheme: light) {
311
+ .card { --card-text: #3b46b8; --card-tint: rgba(74, 85, 214, .12); }
312
+ .card[data-state="running"] { --card-text: #6d4a00; --card-tint: rgba(240, 178, 50, .18); }
313
+ .card[data-state="completed"] { --card-text: #146c3a; --card-tint: rgba(26, 127, 67, .12); }
314
+ .card[data-state="failed"] { --card-text: #a52327; --card-tint: rgba(194, 43, 47, .12); }
315
+ .card[data-state="cancelled"] { --card-text: #545861; --card-tint: rgba(84, 88, 97, .12); }
316
+ }
317
+ .card-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
318
+ .card .title { font-weight: 600; font-size: var(--fs-sm); color: var(--text-strong); }
319
+ .card .chip {
320
+ font-size: 11px;
321
+ line-height: 1.6;
322
+ padding: 0 6px;
323
+ border-radius: var(--r-full);
324
+ color: var(--card-text);
325
+ background: var(--card-tint);
326
+ flex: none;
327
+ }
328
+ .card .body { font-size: var(--fs-sm); color: var(--text); margin-top: 2px; }
329
+ .card .actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); }
330
+ .card .actions button {
331
+ padding: 4px 12px;
332
+ border-radius: var(--r-sm);
333
+ border: 1px solid var(--line);
334
+ background: var(--input);
335
+ color: var(--text);
336
+ cursor: pointer;
337
+ font-size: var(--fs-sm);
338
+ transition: background var(--dur) var(--ease);
339
+ }
340
+ .card .actions button:hover { background: var(--hover); }
341
+ .card .actions button:disabled { opacity: .5; cursor: default; }
342
+
343
+ /* 空态 / 加载态 / 出错态 */
344
+ .state {
345
+ margin: var(--sp-8) auto;
346
+ max-width: 420px;
347
+ padding: var(--sp-6);
348
+ text-align: center;
349
+ color: var(--muted);
350
+ font-size: var(--fs-sm);
351
+ }
352
+ .state .state-title { color: var(--text); font-size: var(--fs-md); font-weight: 600; margin-bottom: var(--sp-1); }
353
+ .state .state-detail { color: var(--muted); word-break: break-word; }
354
+ .state .state-hint { color: var(--faint); margin-top: var(--sp-2); }
355
+ .state .btn {
356
+ margin-top: var(--sp-4);
357
+ padding: 6px 16px;
358
+ border-radius: var(--r-sm);
359
+ border: 1px solid var(--line);
360
+ background: var(--input);
361
+ color: var(--text);
362
+ cursor: pointer;
363
+ }
364
+ .state .btn:hover { background: var(--hover); }
365
+ .spinner {
366
+ width: 22px; height: 22px;
367
+ margin: 0 auto var(--sp-3);
368
+ border-radius: var(--r-full);
369
+ border: 2px solid var(--line);
370
+ border-top-color: var(--accent);
371
+ animation: spin .8s linear infinite;
372
+ }
373
+ @keyframes spin { to { transform: rotate(360deg); } }
374
+ @media (prefers-reduced-motion: reduce) {
375
+ .spinner { animation: none; border-top-color: var(--accent); }
376
+ }
377
+
378
+ /* ---------- 成员 ---------- */
379
+ .members { padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom)); }
380
+ .members h3 {
381
+ margin: var(--sp-4) var(--pad-x) var(--sp-1);
382
+ font-size: var(--fs-xs);
383
+ font-weight: 600;
384
+ color: var(--faint);
385
+ text-transform: uppercase;
386
+ letter-spacing: .06em;
387
+ }
388
+ .member { display: flex; align-items: center; gap: var(--sp-3); padding: 5px var(--pad-x); border-radius: var(--r-sm); margin: 0 var(--sp-2); }
389
+ .member:hover { background: var(--hover-soft); }
390
+ .member .avatar { width: var(--avatar-sm); height: var(--avatar-sm); font-size: var(--fs-sm); }
391
+ .member-info { min-width: 0; }
392
+ .member-name { display: flex; align-items: center; gap: 6px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
393
+ .dot {
394
+ position: absolute;
395
+ right: -2px; bottom: -2px;
396
+ width: 12px; height: 12px;
397
+ border-radius: var(--r-full);
398
+ border: 3px solid var(--surface);
399
+ background: var(--ok);
400
+ }
401
+ .offline { opacity: .5; }
402
+ .offline .dot { background: var(--muted); }
403
+ .status { font-size: var(--fs-xs); color: var(--muted); }
404
+
405
+ /* ---------- 输入区 ---------- */
406
+ .composer { flex: none; padding: var(--sp-2) var(--pad-x) var(--sp-4); padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom)); }
407
+ #thinking { min-height: 20px; padding: 0 var(--pad-x) var(--sp-1); color: var(--muted); font-size: var(--fs-sm); }
408
+ textarea {
409
+ display: block;
410
+ width: 100%;
411
+ resize: none;
412
+ border: 1px solid transparent;
413
+ border-radius: var(--r-md);
414
+ padding: 11px var(--sp-3);
415
+ background: var(--input);
416
+ color: var(--text);
417
+ font: var(--fs-md)/1.45 system-ui, -apple-system, "PingFang SC", sans-serif;
418
+ max-height: 40vh;
419
+ min-height: 44px;
420
+ transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
421
+ }
422
+ textarea::placeholder { color: var(--faint); }
423
+ textarea:hover { border-color: var(--line); }
424
+ textarea:focus { outline: none; border-color: var(--accent); background: var(--bg); }
425
+ textarea:disabled { opacity: .6; cursor: default; }
426
+
427
+ /* ---------- 窄屏:两侧收成抽屉(阈值 768px 来自 U2 交接)---------- */
428
+ .scrim {
429
+ position: fixed;
430
+ inset: 0;
431
+ z-index: 20;
432
+ background: var(--scrim);
433
+ opacity: 0;
434
+ pointer-events: none;
435
+ transition: opacity var(--dur) var(--ease);
436
+ }
437
+ .scrim.show { opacity: 1; pointer-events: auto; }
438
+
439
+ @media (max-width: 768px) {
440
+ body { grid-template-columns: minmax(0, 1fr); }
441
+ #roomsPane, #members {
442
+ position: fixed;
443
+ top: 0;
444
+ bottom: 0;
445
+ z-index: 30;
446
+ width: min(320px, 86vw);
447
+ border: 0;
448
+ box-shadow: var(--shadow);
449
+ will-change: transform;
450
+ transition: transform var(--dur) var(--ease);
451
+ padding-top: env(safe-area-inset-top);
452
+ padding-bottom: env(safe-area-inset-bottom);
453
+ }
454
+ #roomsPane { left: 0; transform: translateX(-100%); }
455
+ #members { right: 0; transform: translateX(100%); }
456
+ body[data-drawer="rooms"] #roomsPane,
457
+ body[data-drawer="members"] #members { transform: translateX(0); }
458
+ /* 44px:验收达标线「手机点按目标 ≥44px」 */
459
+ .icon-btn { display: inline-grid; width: 44px; height: 44px; }
460
+ .mode { max-width: 140px; min-height: 44px; }
461
+ .rooms button { min-height: 44px; }
462
+ .card .actions button { min-height: 44px; }
463
+ .scrim { -webkit-tap-highlight-color: transparent; }
464
+ .pane-head { padding-left: max(var(--pad-x), env(safe-area-inset-left)); }
465
+ .msg { padding-left: max(var(--sp-3), env(safe-area-inset-left)); padding-right: max(var(--sp-3), env(safe-area-inset-right)); }
466
+ .msg .stamp { left: max(var(--sp-3), env(safe-area-inset-left)); }
467
+ .topbar { padding-left: max(var(--sp-3), env(safe-area-inset-left)); padding-right: max(var(--sp-3), env(safe-area-inset-right)); }
468
+ .composer { padding-left: max(var(--sp-3), env(safe-area-inset-left)); padding-right: max(var(--sp-3), env(safe-area-inset-right)); }
469
+ }
470
+
471
+ @media (max-height: 520px) {
472
+ .composer { padding-bottom: var(--sp-2); }
473
+ #log { padding-top: var(--sp-2); }
474
+ }
47
475
  </style>
48
476
  </head>
49
477
  <body>
50
- <aside><div class="head">CoNest</div><nav class="rooms" id="rooms" aria-label="频道"></nav></aside>
478
+ <aside id="roomsPane">
479
+ <div class="pane-head"><span class="brand" aria-hidden="true">C</span>CoNest</div>
480
+ <nav class="rooms" id="rooms" aria-label="频道"></nav>
481
+ </aside>
482
+
51
483
  <main>
52
- <div class="head"><span id="title"># </span><select id="mode" class="mode" aria-label="派活方式"><option value="mixed">混合</option><option value="solo">单模型多进程</option><option value="team">完全团队</option></select></div>
53
- <div id="log" role="log" aria-live="polite"></div>
484
+ <div class="topbar">
485
+ <button class="icon-btn" id="openRooms" type="button" aria-label="打开频道列表" aria-controls="roomsPane" aria-expanded="false">
486
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
487
+ </button>
488
+ <h1 class="title" id="title">#</h1>
489
+ <span class="spacer"></span>
490
+ <select id="mode" class="mode" aria-label="派活方式">
491
+ <option value="mixed">混合</option>
492
+ <option value="solo">单模型多进程</option>
493
+ <option value="team">完全团队</option>
494
+ </select>
495
+ <button class="icon-btn" id="openMembers" type="button" aria-label="打开成员列表" aria-controls="members" aria-expanded="false">
496
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M17 3.13a4 4 0 0 1 0 7.75"/></svg>
497
+ </button>
498
+ </div>
499
+ <div id="log" role="log" aria-live="polite" aria-busy="true"></div>
54
500
  <div id="thinking"></div>
55
- <form id="form"><textarea id="input" rows="1" placeholder="发消息,@成员 让它干活(Enter 发送,Shift+Enter 换行)"></textarea></form>
501
+ <form id="form" class="composer"><textarea id="input" rows="1" placeholder="发消息,@成员 让它干活(Enter 发送,Shift+Enter 换行)" disabled></textarea></form>
56
502
  </main>
57
- <aside class="members" id="members" aria-label="成员"></aside>
503
+
504
+ <aside id="members" class="members" aria-label="成员"></aside>
505
+ <div class="scrim" id="scrim" aria-hidden="true"></div>
506
+
58
507
  <script>
508
+ // 经面板 /dsh/ 转发时页面挂在前缀下:接口和链接都按当前地址带上前缀,直连时前缀为空。
509
+ const ROOT = (location.pathname.match(/^(.*?)\/collab(?:\/|$)/) || ['', ''])[1];
59
510
  const state = { me: 'owner', rooms: [], members: [], thinking: {}, room: null, synced: 0, mode: 'mixed' };
60
511
  const $ = id => document.getElementById(id);
61
512
  // crypto.randomUUID only exists in secure contexts (https/localhost); plain-http LAN/Tailscale pages fall back to getRandomValues.
@@ -66,11 +517,19 @@ const uuid = () => {
66
517
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
67
518
  };
68
519
  const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
69
- const palette = ['#5865f2', '#eb459e', '#3ba55c', '#faa61a', '#ed4245', '#00a8fc', '#9b59b6', '#e67e22'];
520
+ // 身份色只 3 种(design-spec-channels 第 0 节第 3 条:同屏角色色 ≤3);来源:该规范,不用彩虹哈希
521
+ // taste-skill-ignore purple:靛蓝是 design-spec-channels.md 定死的唯一强调色(用户 ClawChat 标杆,不照抄其橙),
522
+ // 按 SKILL.md「已有品牌」条款保留品牌色;taste-skill 明说禁 indigo 是 owner 口味而非研究结论。
523
+ const palette = ['#4F46E5', '#B45309', '#0F766E']; // taste-skill-ignore 靛蓝为 design-spec 唯一强调色(用户标杆),按 SKILL.md 已有品牌条款保留
70
524
  const color = id => palette[[...id].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7) % palette.length];
71
525
  const kindLabel = { dsh: 'DSH', bot: 'Agent', human: '' };
526
+ const cardStateLabel = { pending: '待确认', running: '进行中', completed: '已完成', failed: '失败', cancelled: '已取消' };
527
+ // 5 分钟:同一人连续发言合并头像与昵称的窗口,沿用 Discord 的习惯(交接要求「按同一人连续发言分组」)
528
+ const GROUP_WINDOW_MS = 5 * 60 * 1000;
529
+ // 768px 是 U2 交接里定的窄屏阈值
530
+ const narrow = window.matchMedia('(max-width: 768px)');
72
531
 
73
- const avatarSrc = v => !v ? '' : /^https?:/.test(v) ? v : '/collab/avatars/' + encodeURIComponent(v) + '.png';
532
+ const avatarSrc = v => !v ? '' : /^https?:/.test(v) ? v : ROOT + '/collab/avatars/' + encodeURIComponent(v) + '.png';
74
533
  function avatar(id, name) {
75
534
  const m = state.members.find(m => m.id === id);
76
535
  const a = el('div', 'avatar', (name || id).slice(0, 1)); a.style.background = color(id);
@@ -79,11 +538,42 @@ function avatar(id, name) {
79
538
  return a;
80
539
  }
81
540
 
541
+ const pad2 = n => String(n).padStart(2, '0');
542
+ function fmtTime(ts) {
543
+ const d = new Date(ts * 1000);
544
+ const hm = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
545
+ return d.toDateString() === new Date().toDateString() ? hm : `${d.getMonth() + 1}月${d.getDate()}日 ${hm}`;
546
+ }
547
+
548
+ /* ---------- 窄屏抽屉 ---------- */
549
+ let drawerTrigger = null;
550
+ function setDrawer(which, trigger) {
551
+ if (which && !document.body.dataset.drawer) drawerTrigger = trigger || document.activeElement;
552
+ if (!which) {
553
+ delete document.body.dataset.drawer;
554
+ } else {
555
+ document.body.dataset.drawer = which;
556
+ }
557
+ $('scrim').classList.toggle('show', !!which);
558
+ $('openRooms').setAttribute('aria-expanded', String(which === 'rooms'));
559
+ $('openMembers').setAttribute('aria-expanded', String(which === 'members'));
560
+ const pane = which === 'rooms' ? $('roomsPane') : which === 'members' ? $('members') : null;
561
+ if (pane) pane.querySelector('button, select')?.focus({ preventScroll: true });
562
+ else if (drawerTrigger) { drawerTrigger.focus({ preventScroll: true }); drawerTrigger = null; }
563
+ }
564
+ const closeDrawer = () => setDrawer(null);
565
+ $('openRooms').addEventListener('click', () => setDrawer('rooms', $('openRooms')));
566
+ $('openMembers').addEventListener('click', () => setDrawer('members', $('openMembers')));
567
+ $('scrim').addEventListener('click', closeDrawer);
568
+ document.addEventListener('keydown', e => { if (e.key === 'Escape' && document.body.dataset.drawer) closeDrawer(); });
569
+ narrow.addEventListener('change', closeDrawer);
570
+
82
571
  function renderRooms() {
83
572
  $('rooms').replaceChildren(...state.rooms.map(r => {
84
- const b = el('button', '', '# ' + r.name);
573
+ const b = el('button', '', '');
574
+ b.append(el('span', 'hash', '#'), el('span', 'room-name', r.name));
85
575
  b.setAttribute('aria-current', String(r.id === state.room));
86
- b.onclick = () => openRoom(r.id);
576
+ b.onclick = () => { openRoom(r.id); closeDrawer(); };
87
577
  return b;
88
578
  }));
89
579
  const r = state.rooms.find(r => r.id === state.room);
@@ -97,15 +587,15 @@ function renderMembers() {
97
587
  const channel = room?.parent_id ?? room?.id;
98
588
  const visible = state.members.filter(m => !m.rooms?.length || m.rooms.includes(channel));
99
589
  const groups = [['Agent', visible.filter(m => m.kind !== 'human')], ['真人', visible.filter(m => m.kind === 'human')]];
100
- const nodes = [];
590
+ const nodes = [el('div', 'pane-head', '成员')];
101
591
  for (const [title, list] of groups) {
102
592
  if (!list.length) continue;
103
593
  nodes.push(el('h3', '', `${title} — ${list.length}`));
104
594
  for (const m of list) {
105
595
  const row = el('div', 'member' + (m.online ? '' : ' offline'));
106
596
  const a = avatar(m.id, m.name); a.append(el('span', 'dot'));
107
- const info = el('div');
108
- const name = el('div', '', m.name);
597
+ const info = el('div', 'member-info');
598
+ const name = el('div', 'member-name', m.name);
109
599
  if (kindLabel[m.kind]) name.append(el('span', 'tag', kindLabel[m.kind]));
110
600
  info.append(name, el('div', 'status', busy.has(m.id) ? '正在处理…' : (m.online ? '在线' : '离线')));
111
601
  row.append(a, info);
@@ -118,25 +608,35 @@ function renderMembers() {
118
608
  }
119
609
 
120
610
  function messageNode(m) {
121
- const row = el('div', 'msg' + (m.author_kind === 'system' ? ' system' : ''));
611
+ const system = m.author_kind === 'system';
612
+ const row = el('div', 'msg' + (system ? ' system' : ''));
122
613
  row.dataset.id = m.id;
614
+ row.dataset.author = m.author;
615
+ row.dataset.ts = m.created_at;
616
+ row.dataset.system = system ? '1' : '0';
123
617
  const member = state.members.find(x => x.id === m.author);
124
618
  row.append(avatar(m.author, m.name));
125
619
  const col = el('div');
126
- const head = el('div', 'who', m.name);
127
- if (member && kindLabel[member.kind]) head.append(el('span', 'tag', kindLabel[member.kind]));
128
- head.append(el('span', 'when', new Date(m.created_at * 1000).toLocaleString()));
620
+ const head = el('div', 'head');
621
+ const who = el('span', 'who', m.name);
622
+ if (member && kindLabel[member.kind]) who.append(el('span', 'tag', kindLabel[member.kind]));
623
+ head.append(who, el('span', 'when', fmtTime(m.created_at)));
129
624
  col.append(head);
130
625
  if (m.card) {
131
626
  let meta = {}; try { meta = JSON.parse(m.card); } catch {}
132
- const card = el('div', 'card ' + (meta.state || ''));
133
- card.append(el('div', 'title', meta.title || ''), el('div', 'body', m.text));
627
+ const card = el('div', 'card');
628
+ card.dataset.state = meta.state || '';
629
+ const titleRow = el('div', 'card-head');
630
+ titleRow.append(el('span', 'title', meta.title || ''));
631
+ const label = cardStateLabel[meta.state];
632
+ if (label) titleRow.append(el('span', 'chip', label));
633
+ card.append(titleRow, el('div', 'body', m.text));
134
634
  // Pending permission card: one button per action, posting the decision (same route as a text reply).
135
635
  if (meta.kind === 'permission' && meta.state === 'pending' && Array.isArray(meta.actions)) {
136
636
  const bar = el('div', 'actions');
137
637
  for (const a of meta.actions) {
138
638
  const b = el('button', '', a.label);
139
- b.onclick = () => { bar.querySelectorAll('button').forEach(x => { x.disabled = true; }); api(`/collab/api/messages/${m.id}/permission`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ decision: a.decision }) }); };
639
+ b.onclick = () => { bar.querySelectorAll('button').forEach(x => { x.disabled = true; }); api(`${ROOT}/collab/api/messages/${m.id}/permission`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ decision: a.decision }) }); };
140
640
  bar.append(b);
141
641
  }
142
642
  card.append(bar);
@@ -147,6 +647,26 @@ function messageNode(m) {
147
647
  return row;
148
648
  }
149
649
 
650
+ // 同一人连续发言:后续消息不再重复头像和昵称,悬停时在左侧显示时间。
651
+ function regroup() {
652
+ let prev = null;
653
+ for (const node of $('log').children) {
654
+ if (!node.classList || !node.classList.contains('msg')) { prev = null; continue; }
655
+ const ts = Number(node.dataset.ts);
656
+ const system = node.dataset.system === '1';
657
+ const cont = !!prev && !system && !prev.system && prev.author === node.dataset.author
658
+ && new Date(ts).toDateString() === new Date(prev.ts).toDateString()
659
+ && ts - prev.ts > 0 && ts - prev.ts < GROUP_WINDOW_MS;
660
+ node.classList.toggle('cont', cont);
661
+ if (cont && !node.querySelector('.stamp')) {
662
+ const stamp = el('span', 'stamp', fmtTime(ts));
663
+ stamp.setAttribute('aria-hidden', 'true');
664
+ node.append(stamp);
665
+ }
666
+ prev = { author: node.dataset.author, ts, system };
667
+ }
668
+ }
669
+
150
670
  function upsert(m) {
151
671
  if (m.room_id !== state.room) return;
152
672
  const log = $('log');
@@ -154,9 +674,10 @@ function upsert(m) {
154
674
  const existing = log.querySelector(`[data-id="${m.id}"]`);
155
675
  if (existing) existing.replaceWith(messageNode(m));
156
676
  else {
157
- const after = [...log.children].find(n => Number(n.dataset.id) > m.id);
677
+ const after = [...log.children].find(n => n.dataset.id != null && Number(n.dataset.id) > m.id);
158
678
  log.insertBefore(messageNode(m), after || null);
159
679
  }
680
+ regroup();
160
681
  if (stick) log.scrollTop = log.scrollHeight;
161
682
  }
162
683
 
@@ -166,22 +687,47 @@ async function api(path, init) {
166
687
  return r.json();
167
688
  }
168
689
 
690
+ function showState(kind, title, detail, retry) {
691
+ const box = el('div', 'state ' + kind);
692
+ if (kind === 'loading') box.append(el('div', 'spinner'));
693
+ box.append(el('div', 'state-title', title));
694
+ if (detail) box.append(el('div', 'state-detail', detail));
695
+ if (kind === 'empty') box.append(el('div', 'state-hint', '在下面输入框发第一条消息吧'));
696
+ if (retry) { const b = el('button', 'btn', '重试'); b.type = 'button'; b.onclick = retry; box.append(b); }
697
+ return box;
698
+ }
699
+
169
700
  async function openRoom(id) {
170
701
  state.room = id; state.synced = 0;
171
- $('log').replaceChildren();
172
- const setting = await api(`/collab/api/rooms/${id}/mode`);
173
- state.mode = setting.dispatch_mode;
174
- renderRooms(); renderMembers();
175
- const { messages } = await api(`/collab/api/rooms/${id}/messages?limit=100`);
176
- messages.forEach(upsert);
177
- state.synced = messages.length ? messages[messages.length - 1].id : 0;
178
- $('log').scrollTop = $('log').scrollHeight;
702
+ $('log').replaceChildren(showState('loading', '正在加载消息…'));
703
+ $('log').setAttribute('aria-busy', 'true');
704
+ $('input').disabled = true;
705
+ closeDrawer();
706
+ try {
707
+ const setting = await api(`${ROOT}/collab/api/rooms/${id}/mode`);
708
+ state.mode = setting.dispatch_mode;
709
+ renderRooms(); renderMembers();
710
+ const { messages } = await api(`${ROOT}/collab/api/rooms/${id}/messages?limit=100`);
711
+ if (state.room !== id) return;
712
+ const log = $('log');
713
+ log.querySelector('.state')?.remove();
714
+ messages.forEach(upsert);
715
+ state.synced = messages.length ? messages[messages.length - 1].id : 0;
716
+ if (!log.querySelector('.msg')) log.append(showState('empty', '还没有消息', ''));
717
+ $('log').setAttribute('aria-busy', 'false');
718
+ $('input').disabled = false;
719
+ $('log').scrollTop = $('log').scrollHeight;
720
+ } catch (err) {
721
+ if (state.room !== id) return;
722
+ $('log').replaceChildren(showState('error', '加载失败', err.message, () => openRoom(id)));
723
+ $('log').setAttribute('aria-busy', 'false');
724
+ }
179
725
  }
180
726
 
181
727
  $('mode').addEventListener('change', async (e) => {
182
728
  if (state.room == null) return;
183
729
  try {
184
- const result = await api(`/collab/api/rooms/${state.room}/mode`, {
730
+ const result = await api(`${ROOT}/collab/api/rooms/${state.room}/mode`, {
185
731
  method: 'PATCH', headers: { 'content-type': 'application/json' },
186
732
  body: JSON.stringify({ dispatch_mode: e.target.value }),
187
733
  });
@@ -195,7 +741,7 @@ async function catchUp() {
195
741
  const room = state.room;
196
742
  if (room == null) return;
197
743
  for (;;) {
198
- const { messages } = await api(`/collab/api/rooms/${room}/messages?after=${state.synced}&limit=200`);
744
+ const { messages } = await api(`${ROOT}/collab/api/rooms/${room}/messages?after=${state.synced}&limit=200`);
199
745
  if (state.room !== room) return;
200
746
  messages.forEach(upsert);
201
747
  if (messages.length) state.synced = messages[messages.length - 1].id;
@@ -204,12 +750,12 @@ async function catchUp() {
204
750
  }
205
751
 
206
752
  function connect() {
207
- const es = new EventSource('/collab/api/events');
753
+ const es = new EventSource(ROOT + '/collab/api/events');
208
754
  es.onopen = () => catchUp().catch(console.warn);
209
755
  es.onmessage = (e) => {
210
756
  const ev = JSON.parse(e.data);
211
757
  if (ev.type === 'message') upsert(ev.message);
212
- else if (ev.type === 'deleted') $('log').querySelector(`[data-id="${ev.id}"]`)?.remove();
758
+ else if (ev.type === 'deleted') { $('log').querySelector(`[data-id="${ev.id}"]`)?.remove(); regroup(); }
213
759
  else if (ev.type === 'members') { state.members = ev.members; renderMembers(); }
214
760
  else if (ev.type === 'thinking') { state.thinking[ev.room_id] = ev.members; renderMembers(); }
215
761
  };
@@ -225,7 +771,7 @@ $('form').addEventListener('submit', async (e) => {
225
771
  if (!text || state.room == null) return;
226
772
  $('input').value = ''; $('input').style.height = 'auto';
227
773
  try {
228
- const { message } = await api(`/collab/api/rooms/${state.room}/messages`, {
774
+ const { message } = await api(`${ROOT}/collab/api/rooms/${state.room}/messages`, {
229
775
  method: 'POST', headers: { 'content-type': 'application/json' },
230
776
  body: JSON.stringify({ text, client_key: uuid() }),
231
777
  });
@@ -234,11 +780,14 @@ $('form').addEventListener('submit', async (e) => {
234
780
  });
235
781
 
236
782
  (async () => {
237
- const s = await api('/collab/api/state');
783
+ const s = await api(ROOT + '/collab/api/state');
238
784
  Object.assign(state, { me: s.me, rooms: s.rooms, members: s.members, thinking: s.thinking });
239
785
  connect();
240
786
  await openRoom(s.rooms[0].id);
241
- })().catch(err => { document.body.textContent = '加载失败:' + err.message; });
787
+ })().catch(err => {
788
+ $('log').replaceChildren(showState('error', '加载失败', err.message, () => location.reload()));
789
+ $('log').setAttribute('aria-busy', 'false');
790
+ });
242
791
  </script>
243
792
  </body>
244
- </html>
793
+ </html>