@aicupa/plugin-todo-dependency 1.0.4 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicupa/plugin-todo-dependency",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Set and visualize dependencies between todo items",
5
5
  "description_zh": "设置并可视化待办事项之间的依赖关系",
6
6
  "main": "./service",
package/service.js CHANGED
@@ -11,6 +11,7 @@ module.exports = (api) => {
11
11
  id: node.todo.id,
12
12
  content: node.todo.content,
13
13
  done: node.todo.done,
14
+ focus: node.todo.focus || false,
14
15
  level: node.todo.level,
15
16
  depIds: node.todo.depIds || [],
16
17
  })
@@ -83,6 +84,32 @@ module.exports = (api) => {
83
84
  }
84
85
  },
85
86
 
87
+ async toggleFocus({ todoId, focus, filePath }) {
88
+ try {
89
+ const content = await api.readFile(filePath)
90
+ const data = JSON.parse(content)
91
+ const todotree = data.todotree
92
+
93
+ function findAndUpdate(nodes) {
94
+ for (const node of nodes) {
95
+ if (node.todo && node.todo.id === todoId) {
96
+ node.todo.focus = focus
97
+ return true
98
+ }
99
+ if (node.children?.length && findAndUpdate(node.children)) return true
100
+ }
101
+ return false
102
+ }
103
+
104
+ findAndUpdate(todotree.tree)
105
+ await api.store('todotree', todotree, filePath)
106
+ await api.reload(filePath)
107
+ return { ok: true }
108
+ } catch (e) {
109
+ return { ok: false, error: e.message }
110
+ }
111
+ },
112
+
86
113
  async toggleDone({ todoId, done, filePath }) {
87
114
  try {
88
115
  const content = await api.readFile(filePath)
package/view/index.html CHANGED
@@ -52,7 +52,7 @@
52
52
  padding: 10px 12px; cursor: default;
53
53
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
54
54
  transition: box-shadow 0.2s, transform 0.15s;
55
- display: flex; align-items: flex-start; gap: 10px;
55
+ display: flex; align-items: flex-start; gap: 8px;
56
56
  }
57
57
  .node:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-1px); }
58
58
  .dark .node { background: #2d2d2d; border-color: #3c3c3c; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
@@ -79,7 +79,22 @@
79
79
  .dark .node-check.checked { border-color: #73d13d; background: #73d13d; }
80
80
  .dark .node-check.checked::after { border-color: #2d2d2d; }
81
81
 
82
- .node-body { flex: 1; min-width: 0; }
82
+ .node-body { flex: 1; min-width: 0; position: relative; }
83
+
84
+ .node-focus {
85
+ position: absolute; top: -2px; right: -4px;
86
+ width: 16px; height: 16px; cursor: pointer;
87
+ border: none; background: none; padding: 0;
88
+ color: #bbb; transition: color 0.15s;
89
+ display: flex; align-items: center; justify-content: center;
90
+ opacity: 0; transition: opacity 0.15s, color 0.15s;
91
+ }
92
+ .node:hover .node-focus { opacity: 1; }
93
+ .node-focus.active { opacity: 1; color: #1890ff; }
94
+ .node-focus:hover { color: #1890ff; }
95
+ .dark .node-focus { color: #555; }
96
+ .dark .node-focus:hover { color: #40a9ff; }
97
+ .dark .node-focus.active { color: #40a9ff; }
83
98
 
84
99
  .node-content {
85
100
  font-size: 12px; line-height: 1.5; word-break: break-word;
@@ -91,12 +106,14 @@
91
106
  display: inline-block; font-size: 10px; padding: 1px 6px;
92
107
  border-radius: 10px; margin-top: 5px;
93
108
  }
94
- .badge-pending { background: #fff7e6; color: #d46b08; }
95
- .badge-done { background: #f6ffed; color: #389e0d; }
96
- .badge-blocked { background: #fff1f0; color: #cf1322; }
97
- .dark .badge-pending { background: #3a3000; color: #ffc53d; }
98
- .dark .badge-done { background: #1e3a1e; color: #73d13d; }
99
- .dark .badge-blocked { background: #3a1a1a; color: #ff4d4f; }
109
+ .badge-pending { background: #fff7e6; color: #d46b08; }
110
+ .badge-done { background: #f6ffed; color: #389e0d; }
111
+ .badge-blocked { background: #fff1f0; color: #cf1322; }
112
+ .badge-focus { background: #e6f7ff; color: #096dd9; }
113
+ .dark .badge-pending { background: #3a3000; color: #ffc53d; }
114
+ .dark .badge-done { background: #1e3a1e; color: #73d13d; }
115
+ .dark .badge-blocked { background: #3a1a1a; color: #ff4d4f; }
116
+ .dark .badge-focus { background: #111d2c; color: #40a9ff; }
100
117
 
101
118
  .node.blocked { border-style: dashed; opacity: 0.7; }
102
119
  .dark .node.blocked { opacity: 0.7; }
@@ -109,10 +126,16 @@
109
126
 
110
127
  #edgeSvg { position: absolute; top: 0; left: 0; pointer-events: none; }
111
128
 
129
+ #graphInner { transition: opacity 0.35s ease; }
130
+ #graphInner.fade-out { opacity: 0; }
131
+
112
132
  .empty-state {
113
133
  display: flex; flex-direction: column; align-items: center; justify-content: center;
114
- height: 100%; color: #aaa; padding: 40px 24px; text-align: center; user-select: none;
134
+ position: absolute; inset: 0;
135
+ color: #aaa; padding: 40px 24px; text-align: center; user-select: none;
136
+ opacity: 0; pointer-events: none; transition: opacity 0.35s ease;
115
137
  }
138
+ .empty-state.visible { opacity: 1; pointer-events: auto; }
116
139
  .empty-icon { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.35; }
117
140
  .empty-title { font-size: 15px; font-weight: 500; margin-bottom: 10px; color: #888; }
118
141
  .dark .empty-title { color: #777; }
@@ -128,20 +151,19 @@
128
151
  </div>
129
152
  <div id="warnings"></div>
130
153
  <div id="graphArea">
131
- <div id="graphContainer">
132
- <div class="empty-state" id="emptyState">
133
- <svg class="empty-icon" viewBox="0 0 80 80" fill="none" stroke="currentColor" stroke-width="2">
134
- <rect x="8" y="10" width="24" height="16" rx="4"/>
135
- <rect x="48" y="10" width="24" height="16" rx="4"/>
136
- <rect x="28" y="50" width="24" height="16" rx="4"/>
137
- <line x1="26" y1="26" x2="36" y2="50" stroke-dasharray="3,2"/>
138
- <line x1="54" y1="26" x2="44" y2="50" stroke-dasharray="3,2"/>
139
- <polygon points="36,50 33,45 39,45" fill="currentColor" stroke="none"/>
140
- <polygon points="44,50 41,45 47,45" fill="currentColor" stroke="none"/>
141
- </svg>
142
- <div class="empty-title" data-i18n="emptyTitle"></div>
143
- <div class="empty-hint" data-i18n="emptyHint"></div>
144
- </div>
154
+ <div id="graphContainer"></div>
155
+ <div class="empty-state visible" id="emptyState">
156
+ <svg class="empty-icon" viewBox="0 0 80 80" fill="none" stroke="currentColor" stroke-width="2">
157
+ <rect x="8" y="10" width="24" height="16" rx="4"/>
158
+ <rect x="48" y="10" width="24" height="16" rx="4"/>
159
+ <rect x="28" y="50" width="24" height="16" rx="4"/>
160
+ <line x1="26" y1="26" x2="36" y2="50" stroke-dasharray="3,2"/>
161
+ <line x1="54" y1="26" x2="44" y2="50" stroke-dasharray="3,2"/>
162
+ <polygon points="36,50 33,45 39,45" fill="currentColor" stroke="none"/>
163
+ <polygon points="44,50 41,45 47,45" fill="currentColor" stroke="none"/>
164
+ </svg>
165
+ <div class="empty-title" data-i18n="emptyTitle"></div>
166
+ <div class="empty-hint" data-i18n="emptyHint"></div>
145
167
  </div>
146
168
  </div>
147
169
  </div>
@@ -153,14 +175,18 @@
153
175
  title: '依赖图谱', refresh: '刷新',
154
176
  emptyTitle: '暂无依赖关系',
155
177
  emptyHint: '右键 Todo 节点,选择「设置依赖」<br>即可建立待办之间的依赖关系',
156
- done: '已完成', pending: '待完成', blocked: '阻塞中', notFound: '未找到该 Todo',
178
+ allDoneTitle: '所有依赖任务已完成 🎉',
179
+ allDoneHint: '当前依赖链路中的任务均已完成<br>如有新的依赖关系,请重新创建',
180
+ done: '已完成', pending: '待完成', blocked: '阻塞中', focus: '进行中', unfocus: '取消进行', notFound: '未找到该 Todo',
157
181
  cycleWarning: '检测到循环依赖,部分节点的层级可能不准确',
158
182
  },
159
183
  en: {
160
184
  title: 'Dependency Graph', refresh: 'Refresh',
161
185
  emptyTitle: 'No dependencies yet',
162
186
  emptyHint: 'Right-click a Todo and select "Set Dependencies"<br>to create dependency relationships',
163
- done: 'Done', pending: 'Pending', blocked: 'Blocked', notFound: 'Todo not found',
187
+ allDoneTitle: 'All dependency tasks completed 🎉',
188
+ allDoneHint: 'All tasks in dependency chains are done<br>Create new dependencies if needed',
189
+ done: 'Done', pending: 'Pending', blocked: 'Blocked', focus: 'In Progress', unfocus: 'Cancel Focus', notFound: 'Todo not found',
164
190
  cycleWarning: 'Cycle detected — some nodes may be positioned incorrectly',
165
191
  },
166
192
  }
@@ -243,6 +269,7 @@
243
269
  if (!data) return
244
270
 
245
271
  const { todos, edges: allEdges } = data
272
+ hideEmpty()
246
273
 
247
274
  // Build adjacency (both directions) to find connected components
248
275
  const allIds = new Set()
@@ -275,7 +302,7 @@
275
302
  }
276
303
  const edges = allEdges.filter(([from, to]) => visibleIds.has(from) && visibleIds.has(to))
277
304
  const nodeIds = [...visibleIds]
278
- if (!nodeIds.length) { showEmpty(); return }
305
+ if (!nodeIds.length) { fadeToEmpty(allEdges.length > 0); return }
279
306
 
280
307
  const { layers, hasCycle } = assignLayers(nodeIds, edges)
281
308
  if (hasCycle) {
@@ -285,11 +312,23 @@
285
312
  renderGraph(nodeIds, edges, layout.positions, todos, layout)
286
313
  }
287
314
 
288
- function showEmpty() {
289
- const c = document.getElementById('graphContainer')
315
+ function showEmpty(allDone) {
316
+ document.getElementById('graphContainer').innerHTML = ''
290
317
  const e = document.getElementById('emptyState')
291
- c.innerHTML = ''; c.style.width = ''; c.style.height = ''
292
- c.appendChild(e); e.style.display = 'flex'
318
+ e.querySelector('.empty-title').innerHTML = allDone ? t.allDoneTitle : t.emptyTitle
319
+ e.querySelector('.empty-hint').innerHTML = allDone ? t.allDoneHint : t.emptyHint
320
+ e.classList.add('visible')
321
+ }
322
+
323
+ function hideEmpty() {
324
+ document.getElementById('emptyState').classList.remove('visible')
325
+ }
326
+
327
+ function fadeToEmpty(allDone) {
328
+ const inner = document.getElementById('graphInner')
329
+ if (!inner) { showEmpty(allDone); return }
330
+ inner.classList.add('fade-out')
331
+ inner.addEventListener('transitionend', () => showEmpty(allDone), { once: true })
293
332
  }
294
333
 
295
334
  // ── Layout algorithm ──
@@ -352,6 +391,13 @@
352
391
  autoScan()
353
392
  }
354
393
 
394
+ async function toggleTodoFocus(todoId, focus) {
395
+ try {
396
+ await callPlugin('toggleFocus', { todoId, focus, filePath: currentFilePath })
397
+ } catch {}
398
+ autoScan()
399
+ }
400
+
355
401
  function renderGraph(nodeIds, edges, positions, todoData, layout) {
356
402
  const container = document.getElementById('graphContainer')
357
403
  container.innerHTML = ''
@@ -442,6 +488,18 @@
442
488
  }
443
489
 
444
490
  const body = document.createElement('div'); body.className = 'node-body'
491
+
492
+ if (todo && !todo.done && !blocked) {
493
+ const focusBtn = document.createElement('button')
494
+ focusBtn.className = 'node-focus' + (todo.focus ? ' active' : '')
495
+ focusBtn.title = todo.focus ? t.unfocus : t.focus
496
+ focusBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>'
497
+ focusBtn.addEventListener('click', e => {
498
+ e.stopPropagation()
499
+ toggleTodoFocus(id, !todo.focus)
500
+ })
501
+ body.appendChild(focusBtn)
502
+ }
445
503
  const content = document.createElement('div'); content.className = 'node-content'
446
504
  if (todo) {
447
505
  content.textContent = todo.content
@@ -459,6 +517,9 @@
459
517
  } else if (blocked) {
460
518
  sb.className = 'node-badge badge-blocked'
461
519
  sb.textContent = t.blocked
520
+ } else if (todo.focus) {
521
+ sb.className = 'node-badge badge-focus'
522
+ sb.textContent = t.focus
462
523
  } else {
463
524
  sb.className = 'node-badge badge-pending'
464
525
  sb.textContent = t.pending