bothouse-dsh 0.1.44 → 0.1.46

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,59 +2,508 @@
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>
59
508
  const state = { me: 'owner', rooms: [], members: [], thinking: {}, room: null, synced: 0, mode: 'mixed' };
60
509
  const $ = id => document.getElementById(id);
@@ -66,9 +515,17 @@ const uuid = () => {
66
515
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
67
516
  };
68
517
  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'];
518
+ // 身份色只 3 种(design-spec-channels 第 0 节第 3 条:同屏角色色 ≤3);来源:该规范,不用彩虹哈希
519
+ // taste-skill-ignore purple:靛蓝是 design-spec-channels.md 定死的唯一强调色(用户 ClawChat 标杆,不照抄其橙),
520
+ // 按 SKILL.md「已有品牌」条款保留品牌色;taste-skill 明说禁 indigo 是 owner 口味而非研究结论。
521
+ const palette = ['#4F46E5', '#B45309', '#0F766E']; // taste-skill-ignore 靛蓝为 design-spec 唯一强调色(用户标杆),按 SKILL.md 已有品牌条款保留
70
522
  const color = id => palette[[...id].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7) % palette.length];
71
523
  const kindLabel = { dsh: 'DSH', bot: 'Agent', human: '' };
524
+ const cardStateLabel = { pending: '待确认', running: '进行中', completed: '已完成', failed: '失败', cancelled: '已取消' };
525
+ // 5 分钟:同一人连续发言合并头像与昵称的窗口,沿用 Discord 的习惯(交接要求「按同一人连续发言分组」)
526
+ const GROUP_WINDOW_MS = 5 * 60 * 1000;
527
+ // 768px 是 U2 交接里定的窄屏阈值
528
+ const narrow = window.matchMedia('(max-width: 768px)');
72
529
 
73
530
  const avatarSrc = v => !v ? '' : /^https?:/.test(v) ? v : '/collab/avatars/' + encodeURIComponent(v) + '.png';
74
531
  function avatar(id, name) {
@@ -79,11 +536,42 @@ function avatar(id, name) {
79
536
  return a;
80
537
  }
81
538
 
539
+ const pad2 = n => String(n).padStart(2, '0');
540
+ function fmtTime(ts) {
541
+ const d = new Date(ts * 1000);
542
+ const hm = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
543
+ return d.toDateString() === new Date().toDateString() ? hm : `${d.getMonth() + 1}月${d.getDate()}日 ${hm}`;
544
+ }
545
+
546
+ /* ---------- 窄屏抽屉 ---------- */
547
+ let drawerTrigger = null;
548
+ function setDrawer(which, trigger) {
549
+ if (which && !document.body.dataset.drawer) drawerTrigger = trigger || document.activeElement;
550
+ if (!which) {
551
+ delete document.body.dataset.drawer;
552
+ } else {
553
+ document.body.dataset.drawer = which;
554
+ }
555
+ $('scrim').classList.toggle('show', !!which);
556
+ $('openRooms').setAttribute('aria-expanded', String(which === 'rooms'));
557
+ $('openMembers').setAttribute('aria-expanded', String(which === 'members'));
558
+ const pane = which === 'rooms' ? $('roomsPane') : which === 'members' ? $('members') : null;
559
+ if (pane) pane.querySelector('button, select')?.focus({ preventScroll: true });
560
+ else if (drawerTrigger) { drawerTrigger.focus({ preventScroll: true }); drawerTrigger = null; }
561
+ }
562
+ const closeDrawer = () => setDrawer(null);
563
+ $('openRooms').addEventListener('click', () => setDrawer('rooms', $('openRooms')));
564
+ $('openMembers').addEventListener('click', () => setDrawer('members', $('openMembers')));
565
+ $('scrim').addEventListener('click', closeDrawer);
566
+ document.addEventListener('keydown', e => { if (e.key === 'Escape' && document.body.dataset.drawer) closeDrawer(); });
567
+ narrow.addEventListener('change', closeDrawer);
568
+
82
569
  function renderRooms() {
83
570
  $('rooms').replaceChildren(...state.rooms.map(r => {
84
- const b = el('button', '', '# ' + r.name);
571
+ const b = el('button', '', '');
572
+ b.append(el('span', 'hash', '#'), el('span', 'room-name', r.name));
85
573
  b.setAttribute('aria-current', String(r.id === state.room));
86
- b.onclick = () => openRoom(r.id);
574
+ b.onclick = () => { openRoom(r.id); closeDrawer(); };
87
575
  return b;
88
576
  }));
89
577
  const r = state.rooms.find(r => r.id === state.room);
@@ -97,15 +585,15 @@ function renderMembers() {
97
585
  const channel = room?.parent_id ?? room?.id;
98
586
  const visible = state.members.filter(m => !m.rooms?.length || m.rooms.includes(channel));
99
587
  const groups = [['Agent', visible.filter(m => m.kind !== 'human')], ['真人', visible.filter(m => m.kind === 'human')]];
100
- const nodes = [];
588
+ const nodes = [el('div', 'pane-head', '成员')];
101
589
  for (const [title, list] of groups) {
102
590
  if (!list.length) continue;
103
591
  nodes.push(el('h3', '', `${title} — ${list.length}`));
104
592
  for (const m of list) {
105
593
  const row = el('div', 'member' + (m.online ? '' : ' offline'));
106
594
  const a = avatar(m.id, m.name); a.append(el('span', 'dot'));
107
- const info = el('div');
108
- const name = el('div', '', m.name);
595
+ const info = el('div', 'member-info');
596
+ const name = el('div', 'member-name', m.name);
109
597
  if (kindLabel[m.kind]) name.append(el('span', 'tag', kindLabel[m.kind]));
110
598
  info.append(name, el('div', 'status', busy.has(m.id) ? '正在处理…' : (m.online ? '在线' : '离线')));
111
599
  row.append(a, info);
@@ -118,19 +606,29 @@ function renderMembers() {
118
606
  }
119
607
 
120
608
  function messageNode(m) {
121
- const row = el('div', 'msg' + (m.author_kind === 'system' ? ' system' : ''));
609
+ const system = m.author_kind === 'system';
610
+ const row = el('div', 'msg' + (system ? ' system' : ''));
122
611
  row.dataset.id = m.id;
612
+ row.dataset.author = m.author;
613
+ row.dataset.ts = m.created_at;
614
+ row.dataset.system = system ? '1' : '0';
123
615
  const member = state.members.find(x => x.id === m.author);
124
616
  row.append(avatar(m.author, m.name));
125
617
  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()));
618
+ const head = el('div', 'head');
619
+ const who = el('span', 'who', m.name);
620
+ if (member && kindLabel[member.kind]) who.append(el('span', 'tag', kindLabel[member.kind]));
621
+ head.append(who, el('span', 'when', fmtTime(m.created_at)));
129
622
  col.append(head);
130
623
  if (m.card) {
131
624
  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));
625
+ const card = el('div', 'card');
626
+ card.dataset.state = meta.state || '';
627
+ const titleRow = el('div', 'card-head');
628
+ titleRow.append(el('span', 'title', meta.title || ''));
629
+ const label = cardStateLabel[meta.state];
630
+ if (label) titleRow.append(el('span', 'chip', label));
631
+ card.append(titleRow, el('div', 'body', m.text));
134
632
  // Pending permission card: one button per action, posting the decision (same route as a text reply).
135
633
  if (meta.kind === 'permission' && meta.state === 'pending' && Array.isArray(meta.actions)) {
136
634
  const bar = el('div', 'actions');
@@ -147,6 +645,26 @@ function messageNode(m) {
147
645
  return row;
148
646
  }
149
647
 
648
+ // 同一人连续发言:后续消息不再重复头像和昵称,悬停时在左侧显示时间。
649
+ function regroup() {
650
+ let prev = null;
651
+ for (const node of $('log').children) {
652
+ if (!node.classList || !node.classList.contains('msg')) { prev = null; continue; }
653
+ const ts = Number(node.dataset.ts);
654
+ const system = node.dataset.system === '1';
655
+ const cont = !!prev && !system && !prev.system && prev.author === node.dataset.author
656
+ && new Date(ts).toDateString() === new Date(prev.ts).toDateString()
657
+ && ts - prev.ts > 0 && ts - prev.ts < GROUP_WINDOW_MS;
658
+ node.classList.toggle('cont', cont);
659
+ if (cont && !node.querySelector('.stamp')) {
660
+ const stamp = el('span', 'stamp', fmtTime(ts));
661
+ stamp.setAttribute('aria-hidden', 'true');
662
+ node.append(stamp);
663
+ }
664
+ prev = { author: node.dataset.author, ts, system };
665
+ }
666
+ }
667
+
150
668
  function upsert(m) {
151
669
  if (m.room_id !== state.room) return;
152
670
  const log = $('log');
@@ -154,9 +672,10 @@ function upsert(m) {
154
672
  const existing = log.querySelector(`[data-id="${m.id}"]`);
155
673
  if (existing) existing.replaceWith(messageNode(m));
156
674
  else {
157
- const after = [...log.children].find(n => Number(n.dataset.id) > m.id);
675
+ const after = [...log.children].find(n => n.dataset.id != null && Number(n.dataset.id) > m.id);
158
676
  log.insertBefore(messageNode(m), after || null);
159
677
  }
678
+ regroup();
160
679
  if (stick) log.scrollTop = log.scrollHeight;
161
680
  }
162
681
 
@@ -166,16 +685,41 @@ async function api(path, init) {
166
685
  return r.json();
167
686
  }
168
687
 
688
+ function showState(kind, title, detail, retry) {
689
+ const box = el('div', 'state ' + kind);
690
+ if (kind === 'loading') box.append(el('div', 'spinner'));
691
+ box.append(el('div', 'state-title', title));
692
+ if (detail) box.append(el('div', 'state-detail', detail));
693
+ if (kind === 'empty') box.append(el('div', 'state-hint', '在下面输入框发第一条消息吧'));
694
+ if (retry) { const b = el('button', 'btn', '重试'); b.type = 'button'; b.onclick = retry; box.append(b); }
695
+ return box;
696
+ }
697
+
169
698
  async function openRoom(id) {
170
699
  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;
700
+ $('log').replaceChildren(showState('loading', '正在加载消息…'));
701
+ $('log').setAttribute('aria-busy', 'true');
702
+ $('input').disabled = true;
703
+ closeDrawer();
704
+ try {
705
+ const setting = await api(`/collab/api/rooms/${id}/mode`);
706
+ state.mode = setting.dispatch_mode;
707
+ renderRooms(); renderMembers();
708
+ const { messages } = await api(`/collab/api/rooms/${id}/messages?limit=100`);
709
+ if (state.room !== id) return;
710
+ const log = $('log');
711
+ log.querySelector('.state')?.remove();
712
+ messages.forEach(upsert);
713
+ state.synced = messages.length ? messages[messages.length - 1].id : 0;
714
+ if (!log.querySelector('.msg')) log.append(showState('empty', '还没有消息', ''));
715
+ $('log').setAttribute('aria-busy', 'false');
716
+ $('input').disabled = false;
717
+ $('log').scrollTop = $('log').scrollHeight;
718
+ } catch (err) {
719
+ if (state.room !== id) return;
720
+ $('log').replaceChildren(showState('error', '加载失败', err.message, () => openRoom(id)));
721
+ $('log').setAttribute('aria-busy', 'false');
722
+ }
179
723
  }
180
724
 
181
725
  $('mode').addEventListener('change', async (e) => {
@@ -209,7 +753,7 @@ function connect() {
209
753
  es.onmessage = (e) => {
210
754
  const ev = JSON.parse(e.data);
211
755
  if (ev.type === 'message') upsert(ev.message);
212
- else if (ev.type === 'deleted') $('log').querySelector(`[data-id="${ev.id}"]`)?.remove();
756
+ else if (ev.type === 'deleted') { $('log').querySelector(`[data-id="${ev.id}"]`)?.remove(); regroup(); }
213
757
  else if (ev.type === 'members') { state.members = ev.members; renderMembers(); }
214
758
  else if (ev.type === 'thinking') { state.thinking[ev.room_id] = ev.members; renderMembers(); }
215
759
  };
@@ -238,7 +782,10 @@ $('form').addEventListener('submit', async (e) => {
238
782
  Object.assign(state, { me: s.me, rooms: s.rooms, members: s.members, thinking: s.thinking });
239
783
  connect();
240
784
  await openRoom(s.rooms[0].id);
241
- })().catch(err => { document.body.textContent = '加载失败:' + err.message; });
785
+ })().catch(err => {
786
+ $('log').replaceChildren(showState('error', '加载失败', err.message, () => location.reload()));
787
+ $('log').setAttribute('aria-busy', 'false');
788
+ });
242
789
  </script>
243
790
  </body>
244
- </html>
791
+ </html>