@minonl/dia 0.0.14 → 0.0.15

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/dist/cli.js CHANGED
@@ -1899,6 +1899,9 @@ function assertInside(base, p) {
1899
1899
  return abs;
1900
1900
  }
1901
1901
  var TREE_SKIP = new Set([".git", "node_modules", ".data"]);
1902
+ function copyFilter(base) {
1903
+ return (p) => !relative3(base, p).split(sep4).some((s) => TREE_SKIP.has(s));
1904
+ }
1902
1905
  function walkFiles(root) {
1903
1906
  const out = [];
1904
1907
  (function rec(dir) {
@@ -2109,7 +2112,7 @@ function install(from, opts = {}) {
2109
2112
  if (!alreadyInstalled || hashTree(src.dir) !== hashTree(target)) {
2110
2113
  rmSync3(target, { recursive: true, force: true });
2111
2114
  mkdirSync7(target, { recursive: true });
2112
- cpSync(src.dir, target, { recursive: true, dereference: true, filter: (p) => !p.split(sep4).some((s) => TREE_SKIP.has(s)) });
2115
+ cpSync(src.dir, target, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
2113
2116
  }
2114
2117
  const rendered = renderContent(src.dir, target, m) ?? planned;
2115
2118
  const appliedConfig = apply(m.config ?? []);
@@ -2881,7 +2884,7 @@ function install2(from, opts = {}) {
2881
2884
  const staging = join13(idDir, `.staging-${process.pid}-${Date.now().toString(36)}`);
2882
2885
  rmSync4(staging, { recursive: true, force: true });
2883
2886
  mkdirSync10(staging, { recursive: true });
2884
- cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: (p) => !p.split(sep5).some((s) => TREE_SKIP.has(s)) });
2887
+ cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
2885
2888
  const stagingHash = hashTree2(staging);
2886
2889
  let reuseTarget = false;
2887
2890
  if (existsSync11(target)) {
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>dia graph · 视觉设计稿</title>
6
+ <title>dia graph · 视觉设计稿 v0.3</title>
7
7
  <style>
8
8
  :root{
9
9
  --bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
@@ -16,36 +16,71 @@
16
16
  }
17
17
  *{box-sizing:border-box}
18
18
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
19
- header{display:flex;align-items:center;gap:16px;padding:16px 22px;border-bottom:1px solid var(--line);background:var(--panel)}
19
+ header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);background:var(--panel);flex-wrap:wrap}
20
20
  header h1{font-size:17px;margin:0;letter-spacing:.2px}
21
21
  header .sub{color:var(--muted);font-size:12px}
22
22
  header .spacer{flex:1}
23
+ .tabs{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
24
+ .tabs button{border:0;background:transparent;color:var(--muted);padding:6px 12px;font:inherit;font-size:12px;cursor:pointer}
25
+ .tabs button.on{background:var(--blue);color:#fff}
23
26
  .pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
24
- main{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
27
+ main{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
25
28
  .panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
26
29
  .toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
27
30
  .seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
28
31
  .seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
29
32
  .seg button.on{background:var(--blue);color:#fff}
33
+ .seg button:disabled{opacity:.35;cursor:not-allowed}
34
+ .win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
35
+ .search{position:relative}
36
+ .search input{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 10px;font:inherit;font-size:12px;width:210px}
37
+ .search .q-list{position:absolute;left:0;top:34px;z-index:20;width:280px;max-height:250px;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);display:none}
38
+ .search .q-list.open{display:block}
39
+ .q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
40
+ .q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
41
+ .q-item .k{color:var(--muted)}
42
+ .q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
43
+ .q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
44
+ .q-empty{padding:10px;font-size:12px;color:var(--muted)}
45
+ .node.hit{outline:2px solid var(--blue);outline-offset:1px}
46
+ .node.dim{opacity:.22}
47
+ .focus-chip{display:none;align-items:center;gap:4px;border:1px solid var(--blue);color:var(--blue);border-radius:999px;padding:3px 4px 3px 10px;font-size:11px}
48
+ .focus-chip.on{display:inline-flex}
49
+ .focus-chip button{border:0;background:transparent;color:var(--blue);cursor:pointer;font:inherit;font-size:11px;padding:0 6px}
50
+ .focus-chip .fd{border:0;background:transparent;color:var(--blue);font:inherit;font-size:11px}
30
51
  .toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
31
- .canvas{position:relative;height:640px;overflow:auto;padding:10px}
32
- svg.edges{position:absolute;inset:0;pointer-events:none}
33
- svg.edges line{stroke:#94a3b8;stroke-width:1.2;opacity:.8}
34
- svg.edges line.depends_on{stroke:var(--blue)}
35
- svg.edges line.touches{stroke:var(--violet);stroke-dasharray:3 4}
36
- svg.edges line.claims{stroke:var(--cyan);stroke-dasharray:5 4}
37
- .node{position:absolute;width:206px;border:1px solid var(--line);border-left:4px solid var(--muted);border-radius:10px;background:var(--panel);padding:9px 11px;box-shadow:var(--shadow);cursor:pointer;transition:transform .12s ease,box-shadow .12s ease}
38
- .node:hover{transform:translateY(-1px);box-shadow:0 10px 28px rgba(17,24,39,.12)}
39
- .node.task.doing{border-left-color:var(--blue)}
40
- .node.task.blocked{border-left-color:var(--red)}
41
- .node.task.review{border-left-color:var(--amber)}
42
- .node.task.done,.node.task.accepted{border-left-color:var(--green)}
52
+ .canvas{position:relative;height:620px;overflow:hidden}
53
+ .canvas-scroll{position:absolute;inset:0;overflow:auto;padding:10px}
54
+ .lane-rail{position:absolute;left:0;top:0;bottom:0;width:110px;z-index:6;pointer-events:none;background:linear-gradient(90deg,var(--panel) 86%,transparent)}
55
+ svg.edges{position:absolute;inset:0;pointer-events:none;overflow:visible}
56
+ .node{position:absolute;width:190px;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:10px;background:var(--panel);padding:8px 10px;box-shadow:var(--shadow);cursor:pointer}
43
57
  .node.path{border-left-color:var(--violet)}
44
58
  .node.session{border-left-color:var(--cyan)}
45
- .node b{display:block;font-size:13px;line-height:1.35;margin-bottom:3px}
59
+ .node b{display:block;font-size:13px;line-height:1.3;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
46
60
  .node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
47
- .badge{display:inline-block;font-size:10px;padding:1px 6px;border-radius:999px;border:1px solid var(--line);color:var(--muted);margin-top:4px}
48
- .col-label{position:absolute;top:10px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
61
+ .progress{height:4px;border-radius:99px;background:var(--line);margin-top:6px;overflow:hidden}
62
+ .progress i{display:block;height:100%;background:var(--blue);background-image:linear-gradient(45deg,rgba(255,255,255,.28) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.28) 50%,rgba(255,255,255,.28) 75%,transparent 75%);background-size:12px 12px;animation:flow 1s linear infinite}
63
+ .progress i.blocked{background-color:var(--red);animation-play-state:paused;opacity:.65}
64
+ .progress i.review{background-color:var(--amber)}
65
+ .progress i.done,.progress i.accepted{background-color:var(--muted);background-image:none;animation:none}
66
+ .progress i.todo{background-color:var(--muted);background-image:none;animation:none}
67
+ @keyframes flow{to{background-position:12px 0}}
68
+ .st{font-weight:500}
69
+ .st.doing{color:var(--ink)} .st.blocked{color:var(--red)} .st.review{color:var(--amber)}
70
+ .st.done,.st.accepted{color:var(--muted)} .st.todo{color:var(--muted)}
71
+ .prio{float:right;font-size:10px;font-weight:600;border-radius:5px;padding:0 6px;color:#fff;margin-left:4px}
72
+ .prio{background:transparent;border:1px solid var(--line)}
73
+ .prio.p0{background:var(--red);border-color:var(--red);color:#fff}
74
+ .prio.p1{border-color:var(--amber);color:var(--amber)}
75
+ .prio.p2{border-color:var(--line);color:var(--muted)}
76
+ .pgroup{position:absolute;left:0;pointer-events:none;border-bottom:1px dashed var(--line)}
77
+ .plane-label{position:absolute;left:20px;width:78px;z-index:6;pointer-events:none;padding:2px 6px 2px 0;background:linear-gradient(90deg,var(--panel) 72%,transparent);transform:translateY(-50%);font-size:13px;font-weight:700;letter-spacing:.1em}
78
+ .plane-label small{display:block;font-size:9px;font-weight:400;letter-spacing:0;opacity:.7;margin-top:2px}
79
+ .legend-overlay{position:absolute;right:12px;bottom:12px;z-index:5;background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid var(--line);border-radius:10px;padding:8px 10px;box-shadow:var(--shadow);font-size:11px;color:var(--muted);display:grid;gap:5px;min-width:190px}
80
+ .legend-overlay .row{display:flex;align-items:center;gap:8px;white-space:nowrap}
81
+ .legend-overlay .sample{width:38px;height:10px;flex:0 0 38px}
82
+ .legend-overlay b{color:var(--ink);font-weight:600}
83
+ .canvas.simple .legend-overlay .detail-row{display:none}
49
84
  aside{display:flex;flex-direction:column;gap:14px}
50
85
  .side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
51
86
  .side-title h2{font-size:13px;margin:0}
@@ -54,21 +89,68 @@
54
89
  .item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
55
90
  .item:last-child{border-bottom:0}
56
91
  .item .k{color:var(--muted)}
57
- .dot{display:inline-block;width:7px;height:7px;border-radius:99px;background:var(--muted);margin-right:6px;vertical-align:1px}
58
- .dot.blue{background:var(--blue)} .dot.red{background:var(--red)} .dot.green{background:var(--green)} .dot.amber{background:var(--amber)}
59
- .legend{padding:10px 14px 14px;color:var(--muted);font-size:12px;display:grid;gap:6px}
60
- .legend i{display:inline-block;width:18px;height:2px;margin-right:8px;vertical-align:3px}
61
92
  .attention{padding:8px 14px 14px}
62
93
  .attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
63
94
  .attention .row:last-child{border-bottom:0}
64
95
  .tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
65
96
  .tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
66
- .detail{padding:14px;display:none}
67
- .detail.show{display:block}
68
- .detail h3{font-size:14px;margin:0 0 6px}
69
- .detail p{margin:0 0 8px;color:var(--muted);font-size:12px}
70
- .kv{display:grid;grid-template-columns:80px 1fr;gap:4px 10px;font-size:12px}
97
+ .attention .row{cursor:pointer}
98
+ .quick{display:flex;gap:6px;margin-top:6px}
99
+ .quick button,.btn{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:7px;padding:3px 8px;font:inherit;font-size:11px;cursor:pointer}
100
+ .quick button.primary,.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}
101
+ .backdrop{position:fixed;inset:0;background:rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .18s;z-index:40}
102
+ .backdrop.show{opacity:1;pointer-events:auto}
103
+ .drawer{position:fixed;top:0;right:0;bottom:0;width:440px;max-width:92vw;background:var(--panel);border-left:1px solid var(--line);box-shadow:-12px 0 40px rgba(0,0,0,.25);transform:translateX(100%);transition:transform .2s ease;z-index:50;display:flex;flex-direction:column}
104
+ .drawer.show{transform:none}
105
+ .drawer-head{display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
106
+ .drawer-head h3{font-size:15px;margin:0 0 3px}
107
+ .drawer-head .meta{color:var(--muted);font-size:11px}
108
+ .drawer-head .close{margin-left:auto;border:0;background:transparent;color:var(--muted);font-size:18px;cursor:pointer}
109
+ .drawer-tabs{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid var(--line)}
110
+ .drawer-tabs button{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:7px;padding:4px 10px;font:inherit;font-size:12px;cursor:pointer}
111
+ .drawer-tabs button.on{background:var(--blue);border-color:var(--blue);color:#fff}
112
+ .drawer-body{padding:14px 16px 24px;overflow:auto}
113
+ .drawer-body h4{font-size:12px;margin:14px 0 6px;color:var(--muted);letter-spacing:.05em;text-transform:uppercase}
114
+ .kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
71
115
  .kv .k{color:var(--muted)}
116
+ .iv-seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;margin-bottom:12px}
117
+ .iv-seg button{flex:1;border:0;background:transparent;color:var(--muted);padding:7px 8px;font:inherit;font-size:12px;cursor:pointer}
118
+ .iv-seg button.on{background:var(--blue);color:#fff}
119
+ .iv-row{display:grid;gap:6px;margin-bottom:10px}
120
+ .iv-row label{font-size:11px;color:var(--muted)}
121
+ .iv-row select,.iv-row textarea{width:100%;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);padding:8px;font:inherit;font-size:12px}
122
+ .iv-row textarea{min-height:96px;resize:vertical}
123
+ .iv-status{color:var(--green);font-size:11px;margin-left:8px}
124
+ .rel-list .item{display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed var(--line);font-size:12px}
125
+ .rel-list .item:last-child{border-bottom:0}
126
+ .rel-list .k{color:var(--muted)}
127
+ .kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
128
+ .kv .k{color:var(--muted)}
129
+ /* timeline */
130
+ .timeline{padding:14px 16px 18px;overflow:hidden;position:relative}
131
+ .tl-grid{display:grid;grid-template-columns:190px minmax(0,1fr);gap:8px;align-items:start}
132
+ .tl-labels{overflow:hidden;position:relative}
133
+ .tl-label-head{height:34px;display:flex;align-items:flex-end;font-size:11px;color:var(--muted);padding-bottom:6px}
134
+ .tl-labels-body{will-change:transform}
135
+ .tl-label-row{height:38px;box-sizing:border-box;padding:6px 8px 4px 0;border-top:1px dashed var(--line);font-size:12px;overflow:hidden}
136
+ .tl-label-row b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
137
+ .tl-label-row span{color:var(--muted);font-size:10px}
138
+ .tl-scroll{overflow:auto;max-height:620px;scrollbar-width:thin}
139
+ .scroll-hint{position:absolute;right:18px;top:10px;z-index:6;background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:3px 9px;font-size:10px;color:var(--muted);box-shadow:var(--shadow)}
140
+ .tl-head{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:end;color:var(--muted);font-size:11px;margin-bottom:8px;position:sticky;top:0;z-index:4;background:var(--panel)}
141
+ .tl-axis{position:relative;height:34px;width:var(--tlw,900px)}
142
+ .tl-axis span{position:absolute;transform:translateX(-50%);white-space:nowrap;line-height:1.2}
143
+ .tl-axis span small{display:block;font-size:9px;opacity:.55}
144
+ .tl-body{position:relative}
145
+ .tl-row{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:start;min-height:38px;border-top:1px dashed var(--line)}
146
+ .tl-track{position:relative;height:28px;overflow:hidden;width:var(--tlw,900px);margin-top:4px}
147
+ .tl-bar{position:absolute;top:7px;height:14px;border-radius:5px;background:var(--blue);opacity:.9;cursor:pointer}
148
+ .tl-bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:5px;background:rgba(255,255,255,.45)}
149
+ .tl-bar.blocked{background:var(--red)} .tl-bar.done,.tl-bar.accepted,.tl-bar.todo{background:var(--muted)}
150
+ .tl-bar.review{background:var(--amber)}
151
+ .tl-bar.critical{outline:2px solid var(--amber);outline-offset:2px}
152
+ .tl-today{position:absolute;top:0;bottom:0;pointer-events:none;z-index:2;border-left:1px dashed var(--red);color:var(--red);font-size:10px;padding-left:4px}
153
+ .tl-caption{color:var(--muted);font-size:12px;padding:10px 0 2px}
72
154
  footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
73
155
  @media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
74
156
  </style>
@@ -76,34 +158,79 @@
76
158
  <body>
77
159
  <header>
78
160
  <h1>dia graph</h1>
79
- <span class="sub">只读依赖图 · 视觉设计稿 v0.1</span>
80
- <span class="pill">静态 mockup · 无网络</span>
161
+ <span class="sub">只读依赖图 + 时间线 · 视觉设计稿 v0.3</span>
162
+ <div class="tabs" id="tabs">
163
+ <button class="on" data-v="graph">关系图</button>
164
+ <button data-v="time">时间线</button>
165
+ </div>
81
166
  <span class="spacer"></span>
82
- <span class="pill">updated 18:42</span>
167
+ <span class="pill">静态 mockup · 无网络</span>
168
+ <span class="pill" id="route">#/graph</span>
169
+ <span class="pill" id="updated">updated 18:42</span>
83
170
  </header>
84
171
 
85
172
  <main>
86
173
  <section class="panel">
87
174
  <div class="toolbar">
88
- <div class="seg" id="filter">
89
- <button class="on" data-f="all">全部</button>
90
- <button data-f="doing">进行中</button>
91
- <button data-f="blocked">阻塞</button>
92
- <button data-f="review">待评审</button>
175
+ <div id="graph-tools" style="display:flex;gap:8px;align-items:center">
176
+ <div class="seg" id="layout">
177
+ <button class="on" data-l="priority">优先级视图</button>
178
+ <button data-l="dep">依赖视图</button>
179
+ </div>
180
+ <div class="seg" id="mode">
181
+ <button class="on" data-m="simple">简约</button>
182
+ <button data-m="detail">详细</button>
183
+ </div>
184
+ <div class="search">
185
+ <input id="q" type="search" placeholder="搜索 ID / 名称 / owner" autocomplete="off">
186
+ <div class="q-list" id="q-list"></div>
187
+ </div>
188
+ <span class="focus-chip" id="focus-chip"><span id="focus-text"></span><select id="focus-fd" class="fd"><option value="1">1 层</option><option value="2" selected>2 层</option><option value="all">全链路</option></select><button id="focus-exit">退出</button></span>
189
+ </div>
190
+ <div id="time-tools" style="display:none;gap:8px;align-items:center">
191
+ <div class="seg" id="zoom">
192
+ <button data-z="-">-</button>
193
+ <button data-z="reset">100%</button>
194
+ <button data-z="+">+</button>
195
+ </div>
93
196
  </div>
94
- <div class="seg" id="density">
95
- <button class="on" data-d="comfortable">舒适</button>
96
- <button data-d="compact">紧凑</button>
197
+ <select class="win-select" id="win" title="按任务开始时间过滤,结束时间不限(两个视图共用)">
198
+ <option value="7">近 7 天</option>
199
+ <option value="14">近 14 天</option>
200
+ <option value="30">近 30 天</option>
201
+ <option value="0">全部时间</option>
202
+ </select>
203
+ <span class="hint" id="hint">按优先级分行(P0 最高);行内从左到右 = 依赖顺序。简约只看任务和依赖,详细展开具体关联。</span>
204
+ </div>
205
+ <div id="view-graph">
206
+ <div class="canvas" id="canvas">
207
+ <div class="canvas-scroll" id="canvas-scroll">
208
+ <svg class="edges" id="edges"></svg>
209
+ </div>
210
+ <div class="lane-rail" id="lane-rail"></div>
211
+ <div class="legend-overlay" id="legend">
212
+ <div class="row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-dep" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#2563eb"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#2563eb" stroke-width="1.6" marker-end="url(#lg-dep)"/></svg><span><b>→ depends_on</b> 单向</span></div>
213
+ <div class="row detail-row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-touch" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#7c3aed"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#7c3aed" stroke-width="1.6" stroke-dasharray="6 4" marker-end="url(#lg-touch)"/></svg><span><b>→ touches</b> 单向</span></div>
214
+ <div class="row detail-row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-claim" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#0891b2"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#0891b2" stroke-width="1.6" stroke-dasharray="2 3" marker-end="url(#lg-claim)"/></svg><span><b>→ claims</b> 单向</span></div>
215
+ <div class="row detail-row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-collab" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#64748b"/></marker></defs><line x1="3" y1="5" x2="32" y2="5" stroke="#64748b" stroke-width="1.6" stroke-dasharray="1 3" marker-start="url(#lg-collab)" marker-end="url(#lg-collab)"/></svg><span><b>↔ collaborates_with</b> 双向(示例)</span></div>
216
+ </div>
97
217
  </div>
98
- <span class="hint">点击节点查看详情 · 边:依赖 / 触碰 / claim</span>
99
218
  </div>
100
- <div class="canvas" id="canvas">
101
- <svg class="edges" id="edges" width="1200" height="640"></svg>
102
- <div class="col-label" style="left:18px">sessions</div>
103
- <div class="col-label" style="left:250px">todo</div>
104
- <div class="col-label" style="left:470px">doing</div>
105
- <div class="col-label" style="left:690px">review</div>
106
- <div class="col-label" style="left:910px">paths</div>
219
+ <div id="view-time" style="display:none">
220
+ <div class="timeline">
221
+ <div class="scroll-hint">← 左右滚动 →</div>
222
+ <div class="tl-grid">
223
+ <div class="tl-labels">
224
+ <div class="tl-label-head">任务</div>
225
+ <div class="tl-labels-body" id="tl-labels-body"></div>
226
+ </div>
227
+ <div class="tl-scroll" id="tl-scroll">
228
+ <div class="tl-head"><div class="tl-axis" id="tl-axis"></div></div>
229
+ <div class="tl-body" id="tl-body"></div>
230
+ </div>
231
+ </div>
232
+ <div class="tl-caption">橙色描边 = 关键路径(数据根迁移 → install.sh 拆分 → launchd);红色虚线 = 今天。</div>
233
+ </div>
107
234
  </div>
108
235
  </section>
109
236
 
@@ -111,110 +238,493 @@
111
238
  <div class="panel">
112
239
  <div class="side-title"><h2>概览</h2><span>live data</span></div>
113
240
  <div class="list">
114
- <div class="item"><span class="k"><i class="dot blue"></i>任务</span><b>7</b></div>
115
- <div class="item"><span class="k"><i class="dot red"></i>阻塞</span><b>1</b></div>
116
- <div class="item"><span class="k"><i class="dot amber"></i>待评审</span><b>1</b></div>
117
- <div class="item"><span class="k"><i class="dot green"></i>完成</span><b>2</b></div>
241
+ <div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>任务</span><b id="stat-tasks">100</b></div>
242
+ <div class="item"><span class="k"><i class="dot" style="background:var(--red)"></i>阻塞</span><b id="stat-blocked">0</b></div>
243
+ <div class="item"><span class="k"><i class="dot" style="background:var(--amber)"></i>待评审</span><b id="stat-review">0</b></div>
244
+ <div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>完成</span><b id="stat-done">0</b></div>
118
245
  <div class="item"><span class="k">claims</span><b>3</b></div>
119
246
  </div>
120
247
  </div>
121
-
122
248
  <div class="panel">
123
249
  <div class="side-title"><h2>需要人</h2><span>attention</span></div>
124
250
  <div class="attention">
125
- <div class="row"><span class="tag">阻塞</span><div><b>install.sh 迁移</b><br><span class="k">等待数据根切换完成</span></div></div>
126
- <div class="row"><span class="tag amber">claim</span><div><b>src/collab.ts</b><br><span class="k">session-a 持有中</span></div></div>
127
- <div class="row"><span class="tag amber">评审</span><div><b>graph TUI</b><br><span class="k">等待独立 verifier</span></div></div>
251
+ <div class="row" data-task="t-3"><span class="tag">阻塞</span><div><b>launchd 安装脚本</b><br><span class="k">等待 install.sh 拆分</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer('t-3','intervene')">介入</button><button onclick="event.stopPropagation();openDrawer('t-3','task')">查看</button></div></div></div>
252
+ <div class="row" data-task="t-4"><span class="tag amber">claim</span><div><b>src/collab.ts</b><br><span class="k">session-a 持有中</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer('t-4','intervene')">联系持有者</button></div></div></div>
253
+ <div class="row" data-task="t-4"><span class="tag amber">评审</span><div><b>graph TUI</b><br><span class="k">等待独立 verifier</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer('t-4','intervene')">开始评审</button></div></div></div>
128
254
  </div>
129
255
  </div>
130
-
131
- <div class="panel">
132
- <div class="side-title"><h2>图例</h2><span>legend</span></div>
133
- <div class="legend">
134
- <div><i style="background:var(--blue)"></i>depends_on(任务依赖)</div>
135
- <div><i style="background:var(--violet)"></i>touches(触碰路径)</div>
136
- <div><i style="background:var(--cyan)"></i>claims(会话声明)</div>
137
- </div>
138
- </div>
139
-
140
- <div class="panel detail" id="detail">
141
- <div class="side-title"><h2>节点详情</h2><button onclick="document.getElementById('detail').classList.remove('show')" style="border:0;background:transparent;color:var(--muted);cursor:pointer">关闭</button></div>
142
- <div class="detail" style="display:block" id="detail-body"></div>
143
- </div>
144
256
  </aside>
145
257
  </main>
146
- <footer>视觉稿:布局 / 信息层级 / 边语义。真实实现需先解除「框架无网络模块」限制并单独评审。</footer>
258
+ <div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
259
+ <aside class="drawer" id="drawer" aria-hidden="true">
260
+ <div class="drawer-head">
261
+ <div><h3 id="drawer-title">任务详情</h3><div class="meta" id="drawer-sub"></div></div>
262
+ <button class="close" onclick="closeDrawer()">×</button>
263
+ </div>
264
+ <div class="drawer-tabs">
265
+ <button data-dtab="task" onclick="setDrawerTab('task')">任务</button>
266
+ <button data-dtab="intervene" onclick="setDrawerTab('intervene')">介入</button>
267
+ </div>
268
+ <div class="drawer-body" id="drawer-body"></div>
269
+ </aside>
270
+ <footer>颜色只表达警告/异常:红=阻塞/P0,橙=待评审/P1;其余状态用中性灰 + 单一蓝色进度。关系图按依赖深度分层,时间线独立展示执行情况。</footer>
147
271
 
148
272
  <script>
149
- const nodes = [
150
- {id:'session:ses_a', kind:'session', label:'session-a', x:20, y:60},
151
- {id:'session:ses_b', kind:'session', label:'session-b', x:20, y:200},
152
- {id:'task:t-1', kind:'task', status:'todo', label:'数据根迁移', x:240, y:40, owner:'ses_a'},
153
- {id:'task:t-2', kind:'task', status:'doing', label:'install.sh 拆分', x:460, y:40, owner:'ses_a'},
154
- {id:'task:t-3', kind:'task', status:'blocked', label:'launchd 安装脚本', x:680, y:40, owner:'ses_b'},
155
- {id:'task:t-4', kind:'task', status:'review', label:'graph TUI', x:680, y:190, owner:'ses_b'},
156
- {id:'task:t-5', kind:'task', status:'done', label:'dia msg M0', x:240, y:190},
157
- {id:'task:t-6', kind:'task', status:'accepted', label:'claim 机制', x:240, y:320},
158
- {id:'task:t-7', kind:'task', status:'doing', label:'handoff + claim', x:460, y:320, owner:'ses_a'},
159
- {id:'path:install.sh', kind:'path', label:'install.sh', x:900, y:40},
160
- {id:'path:src/collab.ts', kind:'path', label:'src/collab.ts', x:900, y:180},
161
- {id:'path:docs/msg.md', kind:'path', label:'docs/msg.md', x:900, y:320}
162
- ];
163
- const edges = [
164
- {from:'task:t-2', to:'task:t-1', kind:'depends_on'},
165
- {from:'task:t-3', to:'task:t-2', kind:'depends_on'},
166
- {from:'task:t-7', to:'task:t-6', kind:'depends_on'},
167
- {from:'task:t-2', to:'path:install.sh', kind:'touches'},
168
- {from:'task:t-4', to:'path:src/collab.ts', kind:'touches'},
169
- {from:'task:t-7', to:'path:src/collab.ts', kind:'touches'},
170
- {from:'session:ses_a', to:'path:src/collab.ts', kind:'claims'},
171
- {from:'session:ses_b', to:'path:install.sh', kind:'claims'}
172
- ];
173
- const canvas = document.getElementById('canvas');
174
- const svg = document.getElementById('edges');
175
- const byId = new Map(nodes.map(n=>[n.id,n]));
176
- function card(n){
177
- const el=document.createElement('div');
178
- el.className='node '+n.kind+(n.status?' '+n.status:'');
179
- el.style.left=n.x+'px'; el.style.top=n.y+'px'; el.dataset.id=n.id;
180
- const meta = n.kind==='task' ? (n.owner?'@'+n.owner:'unassigned') : n.id.split(':')[0];
181
- el.innerHTML='<b>'+esc(n.label)+'</b><div class="meta">'+esc(n.id)+'</div>'+
182
- (n.status?'<span class="badge">'+esc(n.status)+'</span>':'')+
183
- '<div class="meta" style="margin-top:4px">'+esc(meta)+'</div>';
184
- el.onclick=()=>show(n);
273
+ const paths = ['install.sh','src/collab.ts','docs/msg.md','tests/collab.test.ts','kits/dia-msg'];
274
+ const modules = ['collab','graph','claim','release','docs','review','persona','kit'];
275
+ const statusList = ['todo','doing','review','blocked','done','accepted'];
276
+ const tasks = [];
277
+ for (let i=0;i<100;i++){
278
+ const prio = i<6?'P0':i<24?'P1':i<62?'P2':'P3';
279
+ const status = statusList[(i*7)%statusList.length];
280
+ const progress = status==='done'||status==='accepted'?100:status==='todo'?0:(i*13)%95;
281
+ const startD = -15 + ((i*7)%24);
282
+ const endD = Math.min(15, startD + 1 + ((i*3)%8));
283
+ const id = 't-'+String(i+1).padStart(3,'0');
284
+ const deps = i===0 ? [] : ['t-'+String(i-((i%5)+1)).padStart(3,'0')];
285
+ tasks.push({id, label:modules[i%modules.length]+' '+String(i+1).padStart(3,'0'), status, prio, progress, start:startD, end:endD, owner:i%3===0?null:(i%2?'ses_a':'ses_b'), deps});
286
+ }
287
+ const pad=n=>'t-'+String(n+1).padStart(3,'0');
288
+ [[6,1],[7,2],[8,3],[24,7],[25,8],[62,25],[63,26]].forEach(([i,j])=>{ if(tasks[i]&&!tasks[i].deps.includes(pad(j))) tasks[i].deps.push(pad(j)); });
289
+ const taskEdges = [];
290
+ tasks.forEach(t=>t.deps.forEach(d=>taskEdges.push({from:t.id,to:d,kind:'depends_on'})));
291
+ for(let i=0;i<20;i++) taskEdges.push({from:tasks[i*5].id,to:paths[i%paths.length],kind:'touches'});
292
+ for(let i=0;i<12;i++) taskEdges.push({from:i%2?'ses_a':'ses_b',to:paths[i%paths.length],kind:'claims'});
293
+ taskEdges.push({from:'t-001',to:'t-002',kind:'collaborates_with'});
294
+
295
+ const state={view:'graph', layout:'priority', mode:'simple', range:'30d', zoom:28, window:7, q:'', focus:null, fd:2, from:null, to:null};
296
+ const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
297
+ const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
298
+ const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○'};
299
+
300
+ function focusSet(id,depth){
301
+ const by=new Map(tasks.map(t=>[t.id,t]));
302
+ const max=depth==='all'?Infinity:Math.max(1,Number(depth)||2);
303
+ const out=new Set([id]); let frontier=[id];
304
+ for(let step=0; step<max && frontier.length; step++){
305
+ const next=[];
306
+ frontier.forEach(cur=>{
307
+ const t=by.get(cur); if(!t) return;
308
+ (t.deps||[]).forEach(x=>{ if(by.has(x)&&!out.has(x)){ out.add(x); next.push(x); } });
309
+ tasks.forEach(o=>{ if((o.deps||[]).includes(cur)&&!out.has(o.id)){ out.add(o.id); next.push(o.id); } });
310
+ });
311
+ frontier=next;
312
+ }
313
+ return out;
314
+ }
315
+ function visibleTasks(){
316
+ if(state.focus){ const keep=focusSet(state.focus,state.fd); return tasks.filter(t=>keep.has(t.id)); }
317
+ return tasks.filter(t=>state.window<=0 || t.start>=-state.window);
318
+ }
319
+ function searchMatches(){
320
+ const q=(state.q||'').trim().toLowerCase();
321
+ if(!q) return null;
322
+ return tasks.filter(t=>t.id.toLowerCase().includes(q)||t.label.toLowerCase().includes(q)||String(t.owner||'').toLowerCase().includes(q));
323
+ }
324
+ function syncStats(){
325
+ const list=visibleTasks(), st=id=>document.getElementById(id);
326
+ if(!st('stat-tasks')) return;
327
+ st('stat-tasks').textContent = list.length===tasks.length ? String(tasks.length) : list.length+' / '+tasks.length;
328
+ st('stat-blocked').textContent=String(list.filter(t=>t.status==='blocked').length);
329
+ st('stat-review').textContent=String(list.filter(t=>t.status==='review').length);
330
+ st('stat-done').textContent=String(list.filter(t=>t.status==='done'||t.status==='accepted').length);
331
+ }
332
+ function updateHint(){
333
+ const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
334
+ const n=visibleTasks().length;
335
+ if(state.focus){
336
+ h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
337
+ return;
338
+ }
339
+ h.textContent='显示 '+n+' / '+tasks.length+' 个任务 · '+(state.layout==='dep'?'从左到右 = 依赖深度':'按优先级分行(P0 最高)');
340
+ }
341
+ function depths(){
342
+ const list=visibleTasks();
343
+ const by=new Map(list.map(t=>[t.id,t])), memo=new Map();
344
+ const depth=t=>{
345
+ if(!t) return 0;
346
+ if(memo.has(t.id)) return memo.get(t.id);
347
+ memo.set(t.id,0);
348
+ const d=t.deps.length?Math.max(...t.deps.map(x=>{ const dt=by.get(x); return dt?depth(dt):-1; }))+1:0;
349
+ memo.set(t.id,d); return d;
350
+ };
351
+ list.forEach(depth); return memo;
352
+ }
353
+ function levels(){
354
+ return [...new Set(visibleTasks().map(t=>t.prio))].sort((a,b)=>{
355
+ const na=parseInt(String(a).replace(/^P/i,''),10), nb=parseInt(String(b).replace(/^P/i,''),10);
356
+ return (Number.isFinite(na)?na:99)-(Number.isFinite(nb)?nb:99)||String(a).localeCompare(String(b));
357
+ });
358
+ }
359
+ function rowsFor(){
360
+ const list=visibleTasks(), d=depths();
361
+ const gap=state.mode==='detail'?230:145;
362
+ const h=state.mode==='detail'?205:110;
363
+ return levels().map((prio,i)=>{
364
+ const rowTasks=list.filter(t=>t.prio===prio).sort((a,b)=>(d.get(a.id)-d.get(b.id))||a.label.localeCompare(b.label));
365
+ return {prio,y:36+i*gap,h,tasks:rowTasks};
366
+ }).filter(r=>r.tasks.length);
367
+ }
368
+ let cardMetrics={mid:52,h:58}, clickTimer=null;
369
+ function measureCard(){
370
+ const probe=card(tasks[0],-9999,-9999);
371
+ probe.style.visibility='hidden';
372
+ layer.appendChild(probe);
373
+ const prog=probe.querySelector('.progress');
374
+ const mid=prog?prog.offsetTop+prog.offsetHeight/2+probe.clientTop:44;
375
+ const h=probe.offsetHeight||58;
376
+ probe.remove();
377
+ return {mid,h};
378
+ }
379
+ function bandCenter(r, bandH){ return r.y-12+bandH/2; }
380
+ function cardTop(r){
381
+ const bandH=state.mode==='detail'?230:145;
382
+ const center=bandCenter(r,bandH);
383
+ // 详细:进度条中心线对齐泳道中心;简约:卡片盒整体垂直居中
384
+ return state.mode==='detail'?center-cardMetrics.mid:center-cardMetrics.h/2;
385
+ }
386
+ function edgeOffset(){ return state.mode==='detail'?cardMetrics.mid:cardMetrics.h/2; }
387
+ function pos(){
388
+ const rows=rowsFor(), p=new Map();
389
+ rows.forEach(r=>r.tasks.forEach((t,i)=>p.set(t.id,{x:170+i*230,y:cardTop(r)})));
390
+ return {p,rows};
391
+ }
392
+ function card(t,x,y,showPrio=false){
393
+ const el=document.createElement('div'); el.className='node task '+t.status; el.style.left=x+'px'; el.style.top=y+'px';
394
+ const rel=taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
395
+ const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
396
+ const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
397
+ return '<div class="meta" style="margin-top:2px">↳ '+esc(kind)+': '+esc(other)+'</div>';
398
+ }).slice(0,4).join('');
399
+ const badge=showPrio?'<span class="prio '+esc(t.prio.toLowerCase())+'">'+esc(t.prio)+'</span>':'';
400
+ el.innerHTML='<b>'+esc(t.label)+badge+'</b>'+
401
+ '<div class="meta">'+esc(t.id)+' · <span class="st '+esc(t.status)+'">'+stIcon[t.status]+' '+esc(t.status)+'</span> · '+t.progress+'%</div>'+
402
+ '<div class="progress"><i class="'+esc(t.status)+'" style="width:'+t.progress+'%"></i></div>'+
403
+ (state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
404
+ el.dataset.id=t.id;
405
+ el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
406
+ el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); focusOn(t.id); };
185
407
  return el;
186
408
  }
187
- function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))}
188
- function draw(){
189
- [...canvas.querySelectorAll('.node')].forEach(e=>e.remove());
190
- nodes.forEach(n=>canvas.appendChild(card(n)));
191
- const maxX=Math.max(...nodes.map(n=>n.x))+240, maxY=Math.max(...nodes.map(n=>n.y))+120;
409
+ function edgePath(a,b){
410
+ const o=edgeOffset(), x1=a.x+190,y1=a.y+o,x2=b.x,y2=b.y+o;
411
+ if(x2>=x1) return `M${x1} ${y1} H${x1+Math.max(24,(x2-x1)/2)} V${y2} H${x2}`;
412
+ return `M${x1} ${y1} H${x1-30} V${y2} H${x2}`;
413
+ }
414
+ function drawGraph(){
415
+ canvas.classList.toggle('simple', state.mode==='simple');
416
+ if(state.layout==='dep'){ drawDepGraph(); return; }
417
+ cardMetrics=measureCard();
418
+ const {p,rows}=pos();
419
+ [...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
420
+ rail.style.display=''; rail.querySelectorAll('.plane-label').forEach(e=>e.remove());
421
+ const maxX=Math.max(600, ...rows.map(r=>170+(r.tasks.length-1)*230+190+10));
422
+ const maxY=rows.length?Math.max(...rows.map(r=>r.y+r.h))+40:600;
423
+ const bandH=state.mode==='detail'?230:145;
424
+ rows.forEach((r,i)=>{
425
+ const g=document.createElement('div'); g.className='pgroup '+r.prio.toLowerCase();
426
+ g.style.top=(r.y-12)+'px'; g.style.width=maxX+'px'; g.style.height=bandH+'px';
427
+ const alpha=Math.max(0.02, 0.075-i*0.012);
428
+ g.style.background='color-mix(in srgb, var(--blue) '+Math.round(alpha*100)+'%, transparent)';
429
+ layer.appendChild(g);
430
+ const label=document.createElement('div'); label.className='plane-label '+r.prio.toLowerCase();
431
+ label.dataset.y=String(r.y-12+bandH/2); label.style.top=(r.y-12+bandH/2-layer.scrollTop)+'px';
432
+ label.style.color=i===0?'var(--red)':i===1?'var(--amber)':'var(--muted)';
433
+ label.innerHTML=esc(r.prio)+' · '+r.tasks.length+(i===0?'<small>紧急</small>':i===1?'<small>高</small>':'<small>常规</small>');
434
+ rail.appendChild(label);
435
+ r.tasks.forEach((t,j)=>layer.appendChild(card(t,170+j*230,cardTop(r))));
436
+ });
192
437
  svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
193
- svg.innerHTML=edges.map(e=>{
194
- const a=byId.get(e.from), b=byId.get(e.to); if(!a||!b) return '';
195
- return '<line class="'+e.kind+'" x1="'+(a.x+103)+'" y1="'+(a.y+30)+'" x2="'+(b.x+103)+'" y2="'+(b.y+30)+'" />';
438
+ const defs=`<defs>
439
+ <marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
440
+ <marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
441
+ </defs>`;
442
+ const lines=taskEdges.filter(e=>e.kind==='depends_on'||e.kind==='collaborates_with').map(e=>{
443
+ const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
444
+ const style=e.kind==='collaborates_with'
445
+ ? 'stroke="#64748b" stroke-dasharray="1 3" marker-start="url(#ar-collab)" marker-end="url(#ar-collab)"'
446
+ : 'stroke="#2563eb" marker-end="url(#ar-dep)"';
447
+ return `<path d="${edgePath(a,b)}" fill="none" stroke-width="1.6" ${style}/>`;
448
+ }).join('');
449
+ svg.innerHTML=defs+lines;
450
+ document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
451
+ syncStats(); updateHint(); applySearch();
452
+ }
453
+ let drawerTask=null, drawerTab='task', ivChannel='tui';
454
+ function relations(t){
455
+ return taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
456
+ const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
457
+ const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
458
+ return '<div class="item"><span class="k">'+esc(kind)+'</span><span>'+esc(other)+'</span></div>';
459
+ }).join('') || '<p class="k">无边</p>';
460
+ }
461
+ function taskHtml(t){
462
+ return '<div class="kv"><div class="k">status</div><div>'+esc(t.status)+'</div>'+
463
+ '<div class="k">priority</div><div>'+esc(t.prio)+'</div>'+
464
+ '<div class="k">owner</div><div>'+esc(t.owner||'-')+'</div>'+
465
+ '<div class="k">progress</div><div>'+t.progress+'%</div>'+
466
+ '<div class="k">deps</div><div>'+esc(t.deps.join(', ')||'-')+'</div></div>'+
467
+ '<h4>关联</h4><div class="rel-list">'+relations(t)+'</div>';
468
+ }
469
+ function targets(ch){
470
+ if(ch==='dingtalk') return [['group','乐语助人前端组 · 群聊'],['dm','与 session-a 私聊']];
471
+ if(ch==='msg') return [['ses_a','session-a'],['ses_b','session-b']];
472
+ return [['ses_a','session-a · opencode TUI (online)'],['ses_b','session-b · dsh TUI (online)']];
473
+ }
474
+ function interveneHtml(t){
475
+ const chs=[['tui','TUI 会话'],['dingtalk','钉钉消息链'],['msg','dia msg']];
476
+ const opts=targets(ivChannel).map(([v,l])=>'<option value="'+v+'">'+esc(l)+'</option>').join('');
477
+ return '<div class="iv-seg">'+chs.map(([v,l])=>'<button class="'+(ivChannel===v?'on':'')+'" onclick="setChannel(\''+v+'\')">'+l+'</button>').join('')+'</div>'+
478
+ '<div class="iv-row"><label>目标会话 / 消息链</label><select id="iv-target">'+opts+'</select></div>'+
479
+ '<div class="iv-row"><label>介入消息</label><textarea id="iv-text" placeholder="例如:这个任务卡在什么点?需要我做什么?"></textarea></div>'+
480
+ '<div><button class="btn primary" onclick="sendIntervention()">发送介入</button><span class="iv-status" id="iv-status"></span></div>'+
481
+ '<p class="k" style="margin-top:12px">设计稿:发送仅演示。真实接入分别走 TUI 注入 / 钉钉消息链 / dia msg;同一任务可切换通道。</p>';
482
+ }
483
+ function openDrawer(id, tab){
484
+ drawerTask=tasks.find(t=>t.id===id); if(!drawerTask) return;
485
+ drawerTab=tab||'task';
486
+ document.getElementById('drawer-title').textContent='任务详情';
487
+ document.getElementById('drawer-sub').textContent=drawerTask.label+' · '+drawerTask.id+' · '+drawerTask.prio+' · '+drawerTask.status;
488
+ document.getElementById('drawer').classList.add('show');
489
+ document.getElementById('drawer').setAttribute('aria-hidden','false');
490
+ document.getElementById('backdrop').classList.add('show');
491
+ setDrawerTab(drawerTab);
492
+ }
493
+ function setDrawerTab(tab){
494
+ drawerTab=tab;
495
+ document.querySelectorAll('[data-dtab]').forEach(b=>b.classList.toggle('on',b.dataset.dtab===tab));
496
+ const body=document.getElementById('drawer-body');
497
+ body.innerHTML=tab==='intervene'?interveneHtml(drawerTask):taskHtml(drawerTask);
498
+ }
499
+ function setChannel(ch){ ivChannel=ch; setDrawerTab('intervene'); }
500
+ function sendIntervention(){
501
+ const text=(document.getElementById('iv-text')||{}).value||'';
502
+ if(!text.trim()){ document.getElementById('iv-status').textContent='请先输入内容'; return; }
503
+ document.getElementById('iv-status').textContent='已排队(设计稿)';
504
+ }
505
+ function closeDrawer(){
506
+ document.getElementById('drawer').classList.remove('show');
507
+ document.getElementById('drawer').setAttribute('aria-hidden','true');
508
+ document.getElementById('backdrop').classList.remove('show');
509
+ }
510
+ function dayDate(d){
511
+ const base=new Date('2026-11-06T00:00:00');
512
+ const dt=new Date(base.getTime()+d*86400000);
513
+ return ('0'+(dt.getMonth()+1)).slice(-2)+'-'+('0'+dt.getDate()).slice(-2);
514
+ }
515
+ function isoDate(d){ const base=new Date('2026-11-06T00:00:00'); const dt=new Date(base.getTime()+d*86400000); return dt.toISOString().slice(0,10); }
516
+ function timelineWindow(){
517
+ if(state.from!==null&&state.to!==null) return {from:state.from,to:state.to};
518
+ const list=visibleTasks();
519
+ const w=state.window>0?state.window:14;
520
+ const from=-w;
521
+ const maxEnd=list.length?Math.max(...list.map(t=>t.end)):w;
522
+ return {from,to:Math.max(0,maxEnd)+1};
523
+ }
524
+ function drawTimeline(){
525
+ const win=timelineWindow(), span=Math.max(1,win.to-win.from);
526
+ const x=v=>((v-win.from)/span)*100;
527
+ const chartW=Math.max(640, span*state.zoom);
528
+ document.getElementById('view-time').style.setProperty('--tlw', chartW+'px');
529
+ const axis=document.getElementById('tl-axis'); axis.innerHTML='';
530
+ const step=span<=10?1:span<=18?2:3;
531
+ for(let d=win.from;d<=win.to;d+=step){
532
+ const el=document.createElement('span'); el.style.left=x(d)+'%';
533
+ el.innerHTML=dayDate(d)+'<small>'+(d===0?'今天':(d>0?'+'+d+'d':d+'d'))+'</small>';
534
+ if(d===win.from) el.style.transform='translateX(0)'; else if(d+step>win.to) el.style.transform='translateX(-100%)';
535
+ if(d===0)el.style.color='var(--red)'; axis.appendChild(el);
536
+ }
537
+ const body=document.getElementById('tl-body'); body.innerHTML=''; const labelsBody=document.getElementById('tl-labels-body'); labelsBody.innerHTML=''; const critical=new Set(['t-1','t-2','t-3']);
538
+ visibleTasks().slice().sort((a,b)=>a.start-b.start).forEach(t=>{
539
+ const row=document.createElement('div'); row.className='tl-row';
540
+ row.innerHTML='<div class="tl-track"></div>';
541
+ const label=document.createElement('div'); label.className='tl-label-row'; label.innerHTML='<b>'+esc(t.label)+'</b><span>'+esc(t.id)+' · '+esc(t.prio)+' · '+esc(t.status)+'</span>'; labelsBody.appendChild(label);
542
+ const track=row.querySelector('.tl-track');
543
+ const bar=document.createElement('div'); bar.className='tl-bar '+t.status+(critical.has(t.id)?' critical':'');
544
+ bar.style.left=x(t.start)+'%'; bar.style.width=Math.max(1.5,x(t.end)-x(t.start))+'%';
545
+ bar.title=dayDate(t.start)+' → '+dayDate(t.end)+' ('+t.progress+'%)';
546
+ bar.innerHTML='<div class="fill" style="width:'+t.progress+'%"></div>'; bar.onclick=()=>openDrawer(t.id,'task'); track.appendChild(bar);
547
+ if(win.from<=0&&win.to>=0){ const line=document.createElement('div'); line.className='tl-today'; line.style.left=x(0)+'%'; line.style.top='0'; line.style.bottom='0'; line.textContent=''; track.appendChild(line); }
548
+ body.appendChild(row);
549
+ });
550
+ syncStats();
551
+ }
552
+ function drawDepGraph(){
553
+ cardMetrics=measureCard();
554
+ const list=visibleTasks(), d=depths(), groups=new Map();
555
+ list.forEach(t=>{ const k=d.get(t.id)||0; groups.set(k,[...(groups.get(k)||[]),t]); });
556
+ const p=new Map(); const rows=[];
557
+ const gap=14;
558
+ [...groups.keys()].sort((a,b)=>a-b).forEach((depth,ci)=>{
559
+ const all=groups.get(depth).sort((a,b)=>a.prio.localeCompare(b.prio)||a.label.localeCompare(b.label));
560
+ rows.push({depth,all,x:30+ci*230});
561
+ });
562
+ [...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
563
+ rail.innerHTML=''; rail.style.display='none';
564
+ let maxY=0;
565
+ // 先渲染卡片,再按实测高度逐列堆叠,避免详细模式卡片相互重叠
566
+ rows.forEach(r=>{
567
+ let y=60;
568
+ r.all.forEach(t=>{
569
+ const el=card(t,r.x,y,true);
570
+ el.style.top=y+'px';
571
+ layer.appendChild(el);
572
+ p.set(t.id,{x:r.x,y});
573
+ y+=el.offsetHeight+gap;
574
+ });
575
+ maxY=Math.max(maxY,y);
576
+ });
577
+ maxY=Math.max(600,maxY);
578
+ const maxX=Math.max(600,...rows.map(r=>r.x+200+10));
579
+ svg.setAttribute('width',maxX); svg.setAttribute('height',maxY+40);
580
+ const defs=`<defs>
581
+ <marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
582
+ <marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
583
+ </defs>`;
584
+ const lines=taskEdges.filter(e=>e.kind==='depends_on'||e.kind==='collaborates_with').map(e=>{
585
+ const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
586
+ const style=e.kind==='collaborates_with'?'stroke="#64748b" stroke-dasharray="1 3" marker-start="url(#ar-collab)" marker-end="url(#ar-collab)"':'stroke="#2563eb" marker-end="url(#ar-dep)"';
587
+ return `<path d="${edgePath(a,b)}" fill="none" stroke-width="1.6" ${style}/>`;
196
588
  }).join('');
589
+ svg.innerHTML=defs+lines;
590
+ document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
591
+ syncStats(); updateHint(); applySearch();
592
+ }
593
+ function syncLaneLabels(){
594
+ rail.querySelectorAll('.plane-label').forEach(el=>{
595
+ el.style.top=(Number(el.dataset.y||0)-layer.scrollTop)+'px';
596
+ });
597
+ }
598
+ function applySearch(){
599
+ const ms=searchMatches();
600
+ layer.querySelectorAll('.node.task').forEach(el=>{
601
+ el.classList.remove('hit','dim');
602
+ if(!ms) return;
603
+ if(ms.some(t=>t.id===el.dataset.id)) el.classList.add('hit'); else el.classList.add('dim');
604
+ });
605
+ const box=document.getElementById('q-list');
606
+ if(box && box.classList.contains('open')) renderSearchList(ms);
607
+ }
608
+ function renderSearchList(ms){
609
+ const box=document.getElementById('q-list'); if(!box) return;
610
+ const q=(state.q||'').trim();
611
+ const browse=!q;
612
+ const pool=browse?visibleTasks().slice().sort((a,b)=>a.prio.localeCompare(b.prio)||a.start-b.start):ms;
613
+ if(!pool||!pool.length){
614
+ box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
615
+ box.classList.add('open'); return;
616
+ }
617
+ const head=browse?('<div class="q-head">可见 '+pool.length+' 个 · ↑↓ 选择,Enter 聚焦</div>')
618
+ :('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 聚焦</div>');
619
+ box.innerHTML=head+pool.slice(0,20).map(t=>'<div class="q-item" data-id="'+esc(t.id)+'"><span>'+esc(t.label)+'<i class="q-prio">'+esc(t.prio)+'</i></span><span class="k">'+esc(t.id)+'</span></div>').join('');
620
+ box.classList.add('open');
621
+ box.querySelectorAll('.q-item').forEach(el=>el.onclick=()=>focusOn(el.dataset.id));
622
+ }
623
+ function searchListItems(){
624
+ const box=document.getElementById('q-list');
625
+ return box?[...box.querySelectorAll('.q-item')]:[];
626
+ }
627
+ function moveSearchCursor(delta){
628
+ let items=searchListItems();
629
+ if(!items.length){ renderSearchList(searchMatches()); items=searchListItems(); if(!items.length) return; }
630
+ let idx=items.findIndex(el=>el.classList.contains('active'));
631
+ idx=idx<0?0:(idx+delta+items.length)%items.length;
632
+ items.forEach((el,i)=>el.classList.toggle('active',i===idx));
633
+ items[idx].scrollIntoView({block:'nearest'});
634
+ }
635
+ function focusOn(id){
636
+ state.focus=id; state.fd=state.fd||2; state.q='';
637
+ const qi=document.getElementById('q'); if(qi) qi.value='';
638
+ go(state.view,state.mode,state.range,state.zoom);
639
+ }
640
+ function readRoute(){
641
+ const raw=location.hash.replace(/^#\/?/,''); const [path,qs]=raw.split('?');
642
+ const params=new URLSearchParams(qs||location.search);
643
+ const range=(params.get('range')||'30d');
644
+ const zoom=Math.min(84,Math.max(14,parseInt(params.get('zoom')||'28',10)||28));
645
+ const sx=Number(params.get('sx')||0);
646
+ const fromRaw=params.get('from'), toRaw=params.get('to');
647
+ const toOff=v=>v?Math.round((new Date(v+'T00:00:00').getTime()-new Date('2026-11-06T00:00:00').getTime())/86400000):null;
648
+ const winRaw=params.get('w');
649
+ const rangeRaw=params.get('range');
650
+ const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
651
+ const fdRaw=params.get('fd');
652
+ const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
653
+ return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'', focus:params.get('focus')||null, fd, from:toOff(fromRaw), to:toOff(toRaw), task:params.get('task'), tab:params.get('tab')==='intervene'?'intervene':'task'};
654
+ }
655
+ function routeString(view,mode,range,zoom,from,to){
656
+ const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
657
+ if(view==='graph'&&mode==='detail') qs.set('mode','detail');
658
+ if(view==='graph'&&state.layout==='dep') qs.set('layout','dep');
659
+ if(view==='graph'){
660
+ if(state.window!==7) qs.set('w',String(state.window));
661
+ if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
662
+ if(state.focus){ qs.set('focus',state.focus); if(state.fd!==2) qs.set('fd',String(state.fd)); }
663
+ }
664
+ if(view==='time'){
665
+ if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
666
+ if(state.window!==7) qs.set('w',String(state.window));
667
+ if(zoom&&zoom!==28) qs.set('zoom',String(zoom));
668
+ }
669
+ return '#/'+p+(qs.toString()?'?'+qs.toString():'');
670
+ }
671
+ function applyRoute(){
672
+ const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.focus=r.focus; state.fd=r.fd; state.from=r.from; state.to=r.to;
673
+ document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
674
+ document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
675
+ document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
676
+ document.getElementById('view-graph').style.display=r.view==='graph'?'':'none';
677
+ document.getElementById('view-time').style.display=r.view==='time'?'':'none';
678
+ document.getElementById('graph-tools').style.display=r.view==='graph'?'flex':'none';
679
+ document.getElementById('time-tools').style.display=r.view==='time'?'flex':'none';
680
+ const zbtn=document.querySelector('#zoom button[data-z="reset"]'); if(zbtn) zbtn.textContent=Math.round(r.zoom/28*100)+'%';
681
+ const dec=document.querySelector('#zoom button[data-z="-"]'), inc=document.querySelector('#zoom button[data-z="+"]'); if(dec) dec.disabled=r.zoom<=14; if(inc) inc.disabled=r.zoom>=84;
682
+ if(r.view!=='graph') document.getElementById('hint').textContent='时间线:左右滚动查看;-/+缩放;橙框 = 关键路径';
683
+ const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
684
+ const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
685
+ const chip=document.getElementById('focus-chip');
686
+ if(chip){ chip.classList.toggle('on',!!r.focus); if(r.focus) document.getElementById('focus-text').textContent='聚焦 '+r.focus+' ·'; }
687
+ const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
688
+ const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
689
+ document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
690
+ if(r.view==='time'){ drawTimeline(); const sc=document.getElementById('tl-scroll'), lb=document.getElementById('tl-labels-body'); sc.scrollTop=0; lb.style.transform='translateY(0px)'; if(state.sx) sc.scrollLeft=state.sx; } else drawGraph();
691
+ if(r.task) openDrawer(r.task,r.tab);
692
+ syncLaneLabels();
693
+ }
694
+ function go(view,mode,range,zoom){ const h=routeString(view,mode,range||state.range,zoom||state.zoom,state.from,state.to); if(location.hash===h) applyRoute(); else location.hash=h; }
695
+ document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>go(b.dataset.v==='time'?'time':'graph',state.mode,state.range));
696
+ document.querySelectorAll('#layout button').forEach(b=>b.onclick=()=>{state.layout=b.dataset.l;go(state.view,state.mode,state.range,state.zoom);});
697
+ const winSelEl=document.getElementById('win');
698
+ if(winSelEl) winSelEl.onchange=()=>{ state.window=Number(winSelEl.value); go(state.view,state.mode,state.range,state.zoom); };
699
+ const qInput=document.getElementById('q');
700
+ if(qInput){
701
+ qInput.oninput=()=>{ state.q=qInput.value; applySearch(); renderSearchList(searchMatches()); };
702
+ qInput.onkeydown=e=>{
703
+ if(e.key==='ArrowDown'){ e.preventDefault(); moveSearchCursor(1); }
704
+ else if(e.key==='ArrowUp'){ e.preventDefault(); moveSearchCursor(-1); }
705
+ else if(e.key==='Enter'){
706
+ const items=searchListItems();
707
+ const active=items.find(el=>el.classList.contains('active'))||items[0];
708
+ if(active) focusOn(active.dataset.id);
709
+ } else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
710
+ };
711
+ qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
712
+ qInput.onclick=()=>{ renderSearchList(searchMatches()); };
713
+ qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box) box.classList.remove('open'); },160); };
197
714
  }
198
- function show(n){
199
- const d=document.getElementById('detail'), body=document.getElementById('detail-body');
200
- const out=edges.filter(e=>e.from===n.id||e.to===n.id).map(e=>{
201
- const other=e.from===n.id?e.to:e.from;
202
- return '<div class="item"><span class="k">'+e.kind+'</span><span>'+esc(other)+'</span></div>';
203
- }).join('') || '<p class="muted">无边</p>';
204
- body.innerHTML='<h3>'+esc(n.label)+'</h3><p>'+esc(n.id)+'</p><div class="kv"><div class="k">kind</div><div>'+esc(n.kind)+'</div><div class="k">status</div><div>'+esc(n.status||'-')+'</div><div class="k">owner</div><div>'+esc(n.owner||'-')+'</div></div><h3 style="margin-top:14px">关联</h3>'+out;
205
- d.classList.add('show');
206
- }
207
- document.querySelectorAll('#filter button').forEach(b=>b.onclick=()=>{
208
- document.querySelectorAll('#filter button').forEach(x=>x.classList.remove('on')); b.classList.add('on');
209
- const f=b.dataset.f;
210
- nodes.forEach(n=>{ if(n.kind==='task'){ const el=document.querySelector('[data-id="'+n.id+'"]'); if(el) el.style.display=(f==='all'||n.status===f)?'':'none'; }});
211
- });
212
- document.querySelectorAll('#density button').forEach(b=>b.onclick=()=>{
213
- document.querySelectorAll('#density button').forEach(x=>x.classList.remove('on')); b.classList.add('on');
214
- const compact=b.dataset.d==='compact';
215
- document.querySelectorAll('.node').forEach(el=>{el.style.padding=compact?'5px 8px':'9px 11px';el.style.width=compact?'180px':'206px';});
216
- });
217
- draw();
715
+ const fdSel=document.getElementById('focus-fd');
716
+ if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
717
+ const fxBtn=document.getElementById('focus-exit');
718
+ if(fxBtn) fxBtn.onclick=()=>{ state.focus=null; go(state.view,state.mode,state.range,state.zoom); };
719
+ document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
720
+ document.querySelectorAll('#zoom button').forEach(b=>b.onclick=()=>{ const z=b.dataset.z==='+'?Math.min(84,state.zoom+8):b.dataset.z==='-'?Math.max(14,state.zoom-8):28; go('time',state.mode,state.range,z); });
721
+ layer.addEventListener('scroll',syncLaneLabels,{passive:true});
722
+ const tlScroller=document.getElementById('tl-scroll');
723
+ tlScroller.addEventListener('scroll',()=>{ const lb=document.getElementById('tl-labels-body'); if(lb) lb.style.transform='translateY(-'+tlScroller.scrollTop+'px)'; },{passive:true});
724
+ document.querySelectorAll('.attention .row[data-task]').forEach(r=>r.onclick=()=>openDrawer(r.dataset.task,'intervene'));
725
+ window.addEventListener('hashchange',applyRoute);
726
+ applyRoute();
727
+ if(document.fonts&&document.fonts.ready){ document.fonts.ready.then(()=>{ if(state.view==='graph'){ if(state.layout==='dep') drawDepGraph(); else drawGraph(); } }); }
218
728
  </script>
219
729
  </body>
220
730
  </html>
@@ -1,61 +1,160 @@
1
- # dia graph Web 视觉设计稿(v0.1)
1
+ # dia graph Web 视觉设计稿(v0.3)
2
2
 
3
- > 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(直接用浏览器打开,无网络/无依赖)
4
- > 数据契约:`dia graph --json`(schema 1);本稿只做视觉与信息层级,不是实现。
3
+ > 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(浏览器直接打开,无网络/无依赖)
4
+ > 数据契约:`dia graph --json`(schema 1);时间线所需 start/end 为后续扩展字段。
5
5
 
6
- ## 1. 布局
6
+ ## 1. 两个视图
7
7
 
8
- ```
9
- ┌ header: dia graph · 只读 · 更新时间 ──────────────────────────┐
10
- ├ 工具栏:状态过滤 | 密度 | 提示 │
11
- ├ 主画布(卡片 + SVG 边) ┌ 右侧 320px ────┐
12
- │ sessions | todo | doing | review | paths │ 概览 / 需要人 │
13
- │ │ 图例 / 节点详情 │
14
- └ footer: 数据来源 / 约束 ───────────────────┴────────────────┘
15
- ```
8
+ 顶部 tab:
16
9
 
17
- - 主画布按状态分列:session → todo → doing → review → path;边的方向从左到右。
18
- - 右侧固定 320px:概览计数、attention(阻塞/claim/待评审)、图例、节点详情。
19
- - 节点卡片颜色只表达 **kind + status**,不用于装饰。
10
+ - **关系图**:回答“谁依赖谁、先后顺序、优先级”;
11
+ - **时间线**:独立甘特图,回答“什么时候做、做到哪、是否延期”。
20
12
 
21
- ## 2. 信息层级
13
+ ### 关系图:按依赖深度分层,不按状态分列
22
14
 
23
- | 层 | 内容 | 设计 |
24
- |---|---|---|
25
- | 第一层 | task 标题 + status | 加粗 13px;状态色只出现在左边框和 badge |
26
- | 第二层 | task id / owner | 11px muted;owner 显示短 id(`@a1b2c3d4`) |
27
- | 第三层 | 依赖/触碰/claim | SVG 边 + 右侧详情;不在卡片里堆文字 |
28
- | attention | 阻塞 / claim 冲突 / 待评审 | 右上角固定区,不藏在 hover 里 |
15
+ - 纵轴 = 优先级,**P0 最高、P1 中间、P2 最下**;每个优先级是一条**通栏泳道色带**,泳道之间用虚线分隔,不再用闭合方框;
16
+ - P0/P1/P2 标签固定在左侧泳道栏、垂直居中;**横向滚动时标签吸住左边缘**(JS 同步 scrollLeft),泳道底色按级别计算(P0 最深,向下递减),可自然扩展 P3/P4;
17
+ - 组内从左到右 = 依赖顺序(按依赖深度排序);
18
+ - **状态不占布局**:用内联图标/文本 + 进度条表达;
19
+ - `简约`:只显示 task + `depends_on`;
20
+ - `详细`:展开 owner、session、path、touches、claims,并在卡片里列具体关联(`depends_on / depended_by / touches / claims / collaborates_with`)。
21
+ - 图例固定在画布右下角:只显示当前模式会用到的边;`→` 单向、`↔` 双向,线型与图上一致。
22
+
23
+ ### 时间线:独立甘特
24
+
25
+ - 每行一个 task,横轴为相对今天的天数(`-8d … today … +4d`);
26
+ - 条长 = start/end,内部填充 = progress;
27
+ - 颜色 = status;橙色描边 = 关键路径;红色虚线 = 今天;
28
+ - 点击条/行可看任务详情。
29
+
30
+ ## 2. 边语义
31
+
32
+ | 关系 | 方向 | 线型 | 颜色 |
33
+ |---|---|---|---|
34
+ | `depends_on` | 单向 `→` | 实线 | 蓝 |
35
+ | `touches` | 单向 `→` | 虚线 `6 4` | 紫 |
36
+ | `claims` | 单向 `→` | 点线 `2 3` | 青 |
37
+ | `collaborates_with` | 双向 `↔` | 点线 `1 3` + 双箭头 | 灰(视觉示例,未进 M1 契约) |
38
+
39
+ ## 3. 为什么不再按状态列/行
29
40
 
30
- ## 3. 交互
41
+ 状态列会把“关系”切碎,且 TODO/DONE、REVIEW/BLOCKED 混在一列没有语义。现在:
31
42
 
32
- - 点击节点 → 右侧详情:kind/status/owner + 关联边列表;
33
- - 顶部过滤:all / doing / blocked / review(客户端过滤,不改数据);
34
- - 密度切换:comfortable / compact;
35
- - 刷新:MVP 静态导出;live 版用 `dia graph --json` 轮询(2s),不做 WebSocket;
36
- - 不做:编辑、拖拽改依赖、写 claim(只读)。
43
+ - 依赖顺序是主结构(拓扑层);
44
+ - 状态是属性(颜色/进度);
45
+ - 优先级是行分组(P0/P1/P2 各一行,最高在最上,虚线框圈组);
46
+ - 时间放在独立时间线,不挤在关系图里。
37
47
 
38
- ## 4. 与现有数据契约的映射
48
+ ## 4. 与数据契约的映射
39
49
 
40
- | UI | `dia graph --json` |
50
+ | UI | 数据 |
41
51
  |---|---|
42
- | 卡片 | `nodes[]`:task/session/path |
52
+ | task 卡片 | `nodes[kind=task]` + status/owner |
43
53
  | 依赖边 | `edges[kind=depends_on]` |
44
- | 触碰边 | `edges[kind=touches]` |
45
- | claim 边 | `edges[kind=claims]` |
46
- | 阻塞提示 | task 节点 `blocked_by[]`(派生,不是边) |
47
- | 环 | `cycles[]`,顶部红条提示 |
54
+ | touches / claims | `edges` 对应 kind |
55
+ | 进度 | 后续 `task.progress`(0-100) |
56
+ | 时间线 | 后续 `task.start/end`(相对/ISO) |
57
+ | 关键路径 | 后续由依赖图最长路径计算 |
48
58
 
49
- ## 5. 实现约束(重要)
59
+ ## 5. 实现约束
50
60
 
51
- 仓库的 `tests/static.test.ts` 禁止框架 import `node:http` / `fetch` / 网络模块。因此:
61
+ `tests/static.test.ts` 禁止框架 import `node:http` / `fetch` / 网络模块。因此:
52
62
 
53
- 1. **本稿只是静态 HTML**,不进入 `src/`,不启服务;
54
- 2. 若将来要做真实 Web:
55
- - 方案 A:`dia graph --export-html` 生成静态文件(推荐,不需要网络);
56
- - 方案 B:独立可选包 `@dia/graph-web`(明确例外 + 只绑 loopback + 单独评审),不改核心框架规则。
63
+ 1. 本稿是静态 HTML,不进入 `src/`;
64
+ 2. 真实实现优先 `dia graph --export-html` 生成静态文件;
65
+ 3. live Web 需独立可选包 + 单独评审(loopback only)。
57
66
 
58
67
  ## 6. 后续
59
68
 
60
- - 先确认视觉稿的列布局/信息密度/右侧 attention 位置;
61
- - 确认后先做 `--export-html` 静态导出(零网络、零 daemon),再评估 live 轮询。
69
+ - 先确认“依赖深度分层 + 简约/详细 + 时间线”结构;
70
+ - 确认后做 `--export-html`(零网络),再评估 live 轮询。
71
+
72
+ ## 7. URL 路由
73
+
74
+ 静态稿支持 hash 路由(`file://` 直接可用):
75
+
76
+ ```
77
+ #/graph
78
+ #/graph?mode=simple
79
+ #/graph?mode=detail
80
+ #/timeline
81
+ ```
82
+
83
+ - `view` 也可用 query 形式:`?view=timeline&mode=detail`;
84
+ - 切换 tab / 简约详细会同步更新 hash,便于分享/回链;
85
+ - 真正的独立 path(`/graph`、`/timeline`)需要部署层 rewrite 到同一 HTML,或导出时生成
86
+ `graph.html` / `timeline.html`;静态文件本身不能凭空提供两个 path。
87
+
88
+ ## 8. 颜色语义(v0.4)
89
+
90
+ 原则:**颜色只表达警告/异常,正常状态去色**。
91
+
92
+ | 颜色 | 用途 | 不再用于 |
93
+ |---|---|---|
94
+ | 红 | blocked、P0、异常/失败 | 普通任务状态 |
95
+ | 橙 | review、claim 提醒、P1 | 完成态 |
96
+ | 蓝 | 只有“进行中”的进度条 | done/accepted/todo |
97
+ | 灰 | todo / done / accepted / P2 / 中性文本 | 警告/异常 |
98
+
99
+ - 优先级:P0 红实心;P1 橙描边;P2 灰描边(不抢眼);
100
+ - 状态文本:只有 blocked / review 带颜色,其余中性;
101
+ - 进度条:doing 蓝色条纹动画;blocked 红色暂停/置灰;review 橙色;done/accepted 灰色静态;
102
+ - 时间线同一套语义;关键路径只用橙色描边。
103
+
104
+ ## 9. 任务抽屉与介入(v0.5)
105
+
106
+ 关系图里的卡片**全部是 task**,不再叫“节点详情”。
107
+
108
+ - 点击 task 卡片 → 右侧抽屉「任务详情」,两个 tab:
109
+ - **任务**:status / priority / owner / progress / deps / 关联;
110
+ - **介入**:通道切换 + 目标会话 + 输入框 + 发送(设计稿只演示)。
111
+ - 「需要人」每行带快速操作:
112
+ - 阻塞 → `介入` / `查看`
113
+ - claim → `联系持有者`
114
+ - 待评审 → `开始评审`
115
+ - 介入通道明确区分:
116
+ 1. **TUI 会话**:选择 opencode / dsh 会话,输入后注入;
117
+ 2. **钉钉消息链**:选择群聊/私聊,输入后走钉钉;
118
+ 3. **dia msg**:选择 session id,走 dia 消息邮箱。
119
+ - 同一任务可以在抽屉里切换通道,方便“会话里没人就发钉钉 / 需要留痕就 dia msg”。
120
+ - 深链:`#/graph?task=t-4&tab=intervene` 可直接打开任务抽屉。
121
+
122
+ ## 10. 规模样例(100 tasks / 30 天)
123
+
124
+ mockup 数据改为 **100 个任务、时间跨度 30 天**,用来验证真实规模下的布局:
125
+
126
+ - 关系图有两种布局:**优先级视图**(P0/P1/P2/P3 泳道)和**依赖视图**(按依赖深度从左到右);
127
+ 两种视图**都不折叠任务**(没有 `+N 个` 聚合,任何卡片都能点开),靠时间窗 + 搜索 + 聚焦控制规模;
128
+ - mock 数据补齐跨优先级依赖,确保两种视图都能看到跨层关系;依赖边只连接当前可见任务;
129
+ - 卡片垂直基准分两种模式:**详细** = 进度条中心线(关系虚线/箭头也锚在进度条中心,同一行连线与进度条同高);
130
+ **简约** = 卡片盒整体垂直居中(连线锚在卡片盒中心);两种模式都和泳道中心线/左侧级别标签对齐;
131
+ - 卡片偏移量按实际渲染测量(含字体加载后重排),不写死魔法数;泳道顶部/右侧空白按实际内容收紧。
132
+ - 时间线:按当前时间窗过滤后纵向滚动;轴显示绝对日期(`MM-DD`)并保留相对天数
133
+ (`-7d / 今天 / +Nd`)。
134
+ - 泳道标签在**外层固定 rail**,不参与横向滚动;只随纵向 `scrollTop` 同步位置;
135
+ - 泳道标签为全宽色带 + 虚线分隔,P0/P1/P2/P3 由数据动态生成。
136
+ - 依赖视图按实测卡片高度逐列堆叠,`详细` 模式下卡片不会互相重叠;依赖视图没有泳道,左侧不留级别标签遮罩/占位;
137
+ - 关系视图参数:`#/graph?layout=dep`(依赖视图)/ 默认优先级视图;
138
+ - 时间窗参数(两个视图共用):`#/graph?w=7|14|30|0`(0 = 全部);时间线 `#/timeline?w=7`;
139
+ 显式区间:`#/timeline?from=2026-10-22&to=2026-11-21`。
140
+
141
+ ## 10.1 规模控制:时间窗 + 搜索 + 聚焦
142
+
143
+ 无折叠后,用三种手段控制一次看多少:
144
+
145
+ - **时间窗(默认近 7 天,按开始时间,结束时间不限)**:只显示 `start >= 今天 - N` 的任务,
146
+ 长任务即使结束在很久以后也照常显示;时间线轴自动从 `-N` 延伸到最晚的 `end`;
147
+ - **搜索**:聚焦输入框就出下拉(空时列出当前可见任务,按优先级/开始时间排序),输入 ID / 名称 / owner 实时过滤;
148
+ `↑↓` 选择、`Enter` 聚焦、`Esc` 清空;命中卡片高亮描边、其余置灰,下拉项点击也可聚焦;
149
+ - **聚焦(类 nx graph focus)**:双击卡片或点搜索结果 → 只保留该任务的依赖邻域,
150
+ 芯片可切 `1 层 / 2 层 / 全链路`,「退出」回全量;聚焦状态写进 URL(`focus=t-003&fd=2`);
151
+ - 被过滤掉的任务不计入概览统计,标题显示 `显示 N / 总数`。
152
+
153
+ ## 11. 时间线滚动与缩放
154
+
155
+ - 左侧任务列是**独立固定 pane**(不依赖 sticky),横向滚动时始终可见;任务名顶部对齐;
156
+ - 时间区右上角显示「← 左右滚动 →」提示,避免看不出可滚动;
157
+ - 右侧时间轴在 `tl-scroll` 内横向滚动,表头与内容同步;
158
+ - `zoom` = 每天像素宽度(默认 28,范围 **14–84**,即 50%–300%),控制条 `- / 100% / +`,到边界禁用;
159
+ - URL:`#/timeline?w=7&zoom=40`;
160
+ - 任务纵向滚动,时间窗默认近 7 天开始(结束不限),绝对日期 + 相对天数同时显示。
package/docs/index.md CHANGED
@@ -92,5 +92,5 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
92
92
 
93
93
  | 文档 | 一句话 |
94
94
  |---|---|
95
- | [dia graph Web 视觉设计稿(v0.1)](design/graph-web.md) | 静态 mockup:graph-web-mockup.html(直接用浏览器打开,无网络/无依赖) |
95
+ | [dia graph Web 视觉设计稿(v0.3)](design/graph-web.md) | 静态 mockup:graph-web-mockup.html(浏览器直接打开,无网络/无依赖) |
96
96
 
package/docs/status.md CHANGED
@@ -90,6 +90,7 @@
90
90
  | D0.21 | worktree 抽取 `packages/wt` | **完成** | 7 处 python→node;`dia wt`;4 套测试原样迁移全绿 |
91
91
  | D0.23 | doctor 平台版本自检 | **完成** | `platform:opencode` / `platform:dsh` 报版本(缺宿主 = warn,不判红);`tests/active-doctor.test.ts` |
92
92
  | D0.44 | 配置迁移走 LLM 任务 | **完成** | `docs/migrations/llm-migration.md`:升级只改声明;迁移=任务+判据+回滚;禁止代码里堆版本分支(安全敏感/原子场景例外) |
93
+ | D0.64 | 复制过滤按相对路径判断 | **完成** | 绝对路径匹配 `node_modules` 会误伤 npm 全局安装(kit 整树被过滤 → `E_KIT_EMPTY`);改 `copyFilter(base)` 用相对路径,persona/kit 共用 |
93
94
  | D0.63 | 运行时数据 `.data/` 不进快照、不参与哈希 | **完成** | `TREE_SKIP`(kit/persona 共用):安装复制与 `hashTree`/verify 都跳过 `.data`,防运行时写入造成 verify 假漂移 |
94
95
  | D0.62 | persona 旧版技能软链视为受管 | **完成** | `linkManaged` 同时认 `~/.dia/kits/**` 与 `~/.dia/personas/**`;`persona update` 升级不再需要 `--force` |
95
96
  | D0.61 | kit 空内容拒绝 + doctor 只查引用 kit | **完成** | `kit add` 拒绝空清单/空树(E_MANIFEST/E_KIT_EMPTY);doctor 按 persona lock 的 kits 校验,孤儿不再拖红 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minonl/dia",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "description": "dia (嗲) — lightweight agent framework: knowledge index, audit, session ingest",
6
6
  "bin": {