dsh-todo-time-log 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -6
  2. package/lib/client.js +410 -174
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -83,7 +83,7 @@
83
83
 
84
84
  <p align="center"><img src="https://github.com/FantasyStarry/dsh-todo-time-log/raw/main/docs/images/demo-complete.png" alt="完成与撤销操作示范" width="100%"></p>
85
85
 
86
- 1. 点行内的「完成」(看板里是卡片上的 checkbox),完成时刻自动写入。
86
+ 1. 点行首的勾选圈(看板里是卡片上的勾选框),完成时刻自动写入。
87
87
  2. 底部浮出 toast,「撤销」可以一键退回未完成状态。
88
88
  3. 表格的「完成时间」列立即出现时间;页面高度不变,视线不被打断。
89
89
 
@@ -91,7 +91,7 @@
91
91
 
92
92
  <p align="center"><img src="https://github.com/FantasyStarry/dsh-todo-time-log/raw/main/docs/images/demo-quickadd.png" alt="对话式快速录入操作示范" width="100%"></p>
93
93
 
94
- 1. 打开「对话录入」,先确认工作区 —— `@` 文件补全按工作区隔离。
94
+ 1. 点新建框右侧的「批量」进入批量录入,先确认工作区 —— `@` 文件补全按工作区隔离。
95
95
  2. 一句话可以写多条,支持 `#分类`、`@文件`、`!优先级`、`^类型`。
96
96
  3. 发送后逐条回执;不满意可以整批撤销。
97
97
 
@@ -110,8 +110,9 @@
110
110
 
111
111
  | 能力 | 说明 |
112
112
  | --- | --- |
113
- | 🕒 创建 / 完成时间 | 全自动记录,本地时间 `YYYY-MM-DD HH:mm:ss`,未完成显示 `-`;重复完成、重复取消都会给出明确提示 |
114
- | 🗂 四视图 | 表格 / 看板 / 甘特图 / 日历,同一份数据四种视角;切换视图保留筛选与排序 |
113
+ | 🕒 创建 / 完成时间 | 全自动记录,本地时间 `YYYY-MM-DD HH:mm:ss`,未完成显示 `-`;表格的完成时间列直接给出「用时 / 进行中」两行,重复完成、重复取消都会给出明确提示 |
114
+ | 🗂 四视图 | 表格 / 看板 / 甘特图 / 日历,同一份数据四种视角;切换视图保留筛选与排序,排序统一走表头 |
115
+ | 🧭 行内操作 | 勾选圈完成、点标题展开详情、行尾「⋯」编辑 / 删除;删除二次确认后仍可用 toast 撤销 |
115
116
  | 📊 甘特图 | 纯前端计算(不新增接口):条形 = 创建 → 完成,未完成延伸到「现在」,支持全部 / 近 7 天 / 近 30 天 |
116
117
  | 🏷 分类 · 优先级 · 类型 | 项目分类(可从「已用分类 / 工作区 / 文件夹」建议里选)、高 / 中 / 低优先级、缺陷 / 新功能 / 改进 / 疑问 / 杂项 / 其他 |
117
118
  | 📎 `@工作区文件` | 把待办关联到代码文件;只给文件、不给分类时,自动取文件所在目录作为分类 |
@@ -119,7 +120,8 @@
119
120
  | 🤖 智能体工具 | 智能体可 `list / get / create / complete / uncomplete / update / delete / health`,与 UI 共用同一实现 |
120
121
  | 📦 导入 / 导出 | 页头「数据」:JSON 无损备份、CSV 给 Excel 统计;导入先预览再写入,支持合并 / 仅新增 / 覆盖全部,自动备份并可直接撤销 |
121
122
  | 🔒 本地数据 | `<DSH_PROFILE_DIR 或 ~/.dsh>/todo-time-log/todos.json`;临时文件 + rename 原子替换、写入串行化、跨进程文件锁 |
122
- | 🌗 主题与无障碍 | 全部颜色走 `--dsw-alias-*` token,明暗主题自适应;`aria-live`、`aria-sort`、键盘操作、减少动效偏好 |
123
+ | ✨ 动效 | 列表入场错位、勾选回弹、详情 / 菜单展开、对话框淡入、甘特条「画」出来、筛选计数弹一下;只用 transform / opacity / clip-path,时长 120–200ms |
124
+ | 🌗 主题与无障碍 | 全部颜色走 `--dsw-alias-*` token,明暗主题自适应;`aria-live`、`aria-sort`、键盘操作;`prefers-reduced-motion` 下全部动画与过渡关闭 |
123
125
  | ⌨️ 面板快捷键 | `/` 聚焦搜索、`n` 聚焦新建、`f` 循环切换筛选 |
124
126
 
125
127
  ---
@@ -175,7 +177,7 @@ plugin_manager action=install_bundle target=<本地仓库绝对路径>
175
177
  | 入口 | 怎么用 | 适合 |
176
178
  | --- | --- | --- |
177
179
  | 面板表单 | 顶部输入框 + 分类 / 优先级 / 类型选择器,Enter 创建 | 手动逐条记录 |
178
- | 对话式录入 | 面板右上「对话录入」,一句话写多条,逐条回执 + 撤销 | 一次记一堆 |
180
+ | 对话式录入 | 新建框右侧「批量」,一句话写多条,逐条回执 + 撤销 | 一次记一堆 |
179
181
  | `/todo` 命令 | Harness 主输入框:`/todo 写周报 #工作 lib/store.js` | 不离开对话流 |
180
182
  | 智能体工具 | 让模型调用 `todo_time_log`(`create` / `complete` / `list` …) | 让 AI 代记、代查 |
181
183
 
package/lib/client.js CHANGED
@@ -16,16 +16,12 @@
16
16
  "create.placeholder": "输入待办;可写 #分类 @文件,Enter 创建",
17
17
  "create.action": "添加",
18
18
  "create.actionBusy": "添加中…",
19
+ "create.batch": "批量",
20
+ "create.batchTitle": "批量录入:一句话记多条",
19
21
  "filter.label": "筛选",
20
22
  "filter.all": "全部",
21
23
  "filter.pending": "未完成",
22
24
  "filter.completed": "已完成",
23
- "sort.label": "排序",
24
- "sort.createdAt": "创建时间",
25
- "sort.completedAt": "完成时间",
26
- "direction.asc": "升序",
27
- "direction.desc": "降序",
28
- "direction.toggleAria": "切换排序方向(当前:{direction})",
29
25
  "list.loading": "加载中…",
30
26
  "list.refreshing": "更新中…",
31
27
  "list.error": "加载失败",
@@ -84,9 +80,6 @@
84
80
  "projects.groupFolder": "文件夹",
85
81
  "projects.countOption": "{name}({count})",
86
82
  "group.header": "{project} · {count} 条",
87
- "sort.updatedAt": "更新时间",
88
- "sort.project": "分类",
89
- "sort.priority": "优先级",
90
83
  "priority.label": "优先级",
91
84
  "priority.none": "未设置",
92
85
  "priority.high": "高",
@@ -126,6 +119,10 @@
126
119
  "table.status": "状态",
127
120
  "table.createdAt": "创建时间",
128
121
  "table.completedAt": "完成时间",
122
+ "table.spent": "用时 {duration}",
123
+ "table.elapsed": "进行中 {duration}",
124
+ "table.more": "更多操作",
125
+ "table.moreAria": "打开待办操作:{title}",
129
126
  "table.actions": "操作",
130
127
  "table.detail": "详情",
131
128
  "table.sortAria": "按 {column} 排序",
@@ -246,6 +243,7 @@
246
243
  "notice.created": "已创建待办。",
247
244
  "notice.updated": "已更新待办。",
248
245
  "notice.deleted": "已删除待办。",
246
+ "notice.restored": "已恢复待办。",
249
247
  "notice.completed": "已标记为完成。",
250
248
  "notice.uncompleted": "已取消完成。",
251
249
  "notice.quickAddCreated": "已添加 {count} 条待办",
@@ -307,16 +305,12 @@
307
305
  "create.placeholder": "Type a todo; #project @file supported, Enter to create",
308
306
  "create.action": "Add",
309
307
  "create.actionBusy": "Adding…",
308
+ "create.batch": "Batch",
309
+ "create.batchTitle": "Batch entry: add several todos in one message",
310
310
  "filter.label": "Filter",
311
311
  "filter.all": "All",
312
312
  "filter.pending": "Pending",
313
313
  "filter.completed": "Completed",
314
- "sort.label": "Sort",
315
- "sort.createdAt": "Created",
316
- "sort.completedAt": "Completed",
317
- "direction.asc": "Ascending",
318
- "direction.desc": "Descending",
319
- "direction.toggleAria": "Switch sort direction (currently {direction})",
320
314
  "list.loading": "Loading…",
321
315
  "list.refreshing": "Refreshing…",
322
316
  "list.error": "Failed to load",
@@ -375,9 +369,6 @@
375
369
  "projects.groupFolder": "Folders",
376
370
  "projects.countOption": "{name} ({count})",
377
371
  "group.header": "{project} · {count} item(s)",
378
- "sort.updatedAt": "Updated",
379
- "sort.project": "Project",
380
- "sort.priority": "Priority",
381
372
  "priority.label": "Priority",
382
373
  "priority.none": "None",
383
374
  "priority.high": "High",
@@ -417,6 +408,10 @@
417
408
  "table.status": "Status",
418
409
  "table.createdAt": "Created",
419
410
  "table.completedAt": "Completed",
411
+ "table.spent": "{duration} spent",
412
+ "table.elapsed": "{duration} so far",
413
+ "table.more": "More actions",
414
+ "table.moreAria": "Open actions for {title}",
420
415
  "table.actions": "Actions",
421
416
  "table.detail": "Details",
422
417
  "table.sortAria": "Sort by {column}",
@@ -537,6 +532,7 @@
537
532
  "notice.created": "Todo created.",
538
533
  "notice.updated": "Todo updated.",
539
534
  "notice.deleted": "Todo deleted.",
535
+ "notice.restored": "Todo restored.",
540
536
  "notice.completed": "Marked as completed.",
541
537
  "notice.uncompleted": "Marked as pending.",
542
538
  "notice.quickAddCreated": "Added {count} todo(s)",
@@ -612,13 +608,6 @@
612
608
  { value: "pending", labelKey: "filter.pending" },
613
609
  { value: "completed", labelKey: "filter.completed" }
614
610
  ];
615
- var SORTS = [
616
- { value: "createdAt", labelKey: "sort.createdAt" },
617
- { value: "completedAt", labelKey: "sort.completedAt" },
618
- { value: "updatedAt", labelKey: "sort.updatedAt" },
619
- { value: "project", labelKey: "sort.project" },
620
- { value: "priority", labelKey: "sort.priority" }
621
- ];
622
611
  var PRIORITIES = [
623
612
  { value: "", labelKey: "priority.none" },
624
613
  { value: "high", labelKey: "priority.high" },
@@ -710,6 +699,11 @@
710
699
  font-size: 14px;
711
700
  line-height: 1.6;
712
701
  -webkit-font-smoothing: antialiased;
702
+ /* 动效令牌:短、克制、只服务状态变化(入场 / 浮层 / 勾选)。 */
703
+ --ttl-ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
704
+ --ttl-ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
705
+ --ttl-dur-fast: 120ms;
706
+ --ttl-dur-base: 180ms;
713
707
  }
714
708
  .ttl-scroll {
715
709
  flex: 1;
@@ -790,6 +784,7 @@
790
784
  background: var(--dsw-alias-bg-layer-1);
791
785
  box-shadow: var(--dsw-shadow-lv1);
792
786
  transition: border-color 0.15s, box-shadow 0.15s;
787
+ animation: ttl-fade-in 160ms var(--ttl-ease-out);
793
788
  }
794
789
  .ttl-composer:hover { border-color: var(--dsw-alias-border-l3); }
795
790
  .ttl-composer:focus-within {
@@ -820,6 +815,8 @@
820
815
  color: var(--dsw-alias-label-primary);
821
816
  }
822
817
  .ttl-composer .ttl-button-primary { height: 36px; padding: 0 18px; }
818
+ /* 「批量」是捕获区的模式切换,不是第二个主操作:保持 ghost 层级。 */
819
+ .ttl-composer-batch { height: 32px; padding: 0 8px; }
823
820
  .ttl-create-suggest {
824
821
  position: absolute;
825
822
  top: calc(100% + 6px);
@@ -860,8 +857,9 @@
860
857
  font-size: 13px;
861
858
  line-height: 1;
862
859
  cursor: pointer;
863
- transition: background 0.15s, color 0.15s;
860
+ transition: background 0.15s, color 0.15s, border-color 0.15s, box-shadow 0.15s, transform var(--ttl-dur-fast) var(--ttl-ease-out);
864
861
  }
862
+ .ttl-button:active:not(:disabled) { transform: scale(0.98); }
865
863
  .ttl-button-primary {
866
864
  height: 34px;
867
865
  padding: 0 16px;
@@ -886,7 +884,6 @@
886
884
  .ttl-ghost-button:focus-visible,
887
885
  .ttl-seg-item:focus-visible,
888
886
  .ttl-link:focus-visible,
889
- .ttl-sort-dir:focus-visible,
890
887
  .ttl-toast-action:focus-visible,
891
888
  .ttl-toast-close:focus-visible,
892
889
  .ttl-select select:focus-visible {
@@ -914,7 +911,14 @@
914
911
  border-radius: 0;
915
912
  }
916
913
  .ttl-data .ttl-list > .ttl-board { padding: 12px 12px 16px; }
914
+ /* 工具栏固定两行:第一行视图 + 搜索;第二行视图专属控件 + 全局筛选。 */
917
915
  .ttl-toolbar {
916
+ display: flex;
917
+ flex-direction: column;
918
+ align-items: stretch;
919
+ gap: 8px;
920
+ }
921
+ .ttl-toolbar-row {
918
922
  display: flex;
919
923
  flex-wrap: wrap;
920
924
  align-items: center;
@@ -949,6 +953,9 @@
949
953
  line-height: 1;
950
954
  white-space: nowrap;
951
955
  cursor: pointer;
956
+ transition: background-color var(--ttl-dur-fast) var(--ttl-ease-out),
957
+ color var(--ttl-dur-fast) var(--ttl-ease-out),
958
+ box-shadow var(--ttl-dur-fast) var(--ttl-ease-out);
952
959
  }
953
960
  .ttl-seg-item:hover { color: var(--dsw-alias-label-primary); }
954
961
  .ttl-seg-item[aria-pressed='true'] {
@@ -962,6 +969,7 @@
962
969
  font-size: 11px;
963
970
  font-weight: 500;
964
971
  font-variant-numeric: tabular-nums;
972
+ animation: ttl-count-pop 200ms var(--ttl-ease-out);
965
973
  }
966
974
  .ttl-seg-item[aria-pressed='true'] .ttl-seg-count { color: var(--dsw-alias-label-secondary); }
967
975
  .ttl-select {
@@ -982,26 +990,6 @@
982
990
  font-size: 12px;
983
991
  cursor: pointer;
984
992
  }
985
- .ttl-sort-dir {
986
- display: inline-flex;
987
- align-items: center;
988
- justify-content: center;
989
- width: 28px;
990
- height: 28px;
991
- padding: 0;
992
- border: 0.5px solid var(--dsw-alias-border-l3);
993
- border-radius: var(--dsw-radius-sm);
994
- background: none;
995
- color: var(--dsw-alias-label-secondary);
996
- font: inherit;
997
- font-size: 13px;
998
- line-height: 1;
999
- cursor: pointer;
1000
- }
1001
- .ttl-sort-dir:hover {
1002
- background: var(--dsw-alias-interactive-bg-hover);
1003
- color: var(--dsw-alias-label-primary);
1004
- }
1005
993
  .ttl-refreshing {
1006
994
  min-width: 52px;
1007
995
  font-size: 12px;
@@ -1246,6 +1234,7 @@
1246
1234
  color: var(--dsw-alias-label-tertiary);
1247
1235
  font-size: 12px;
1248
1236
  line-height: 20px;
1237
+ animation: ttl-fade-down 160ms var(--ttl-ease-out);
1249
1238
  }
1250
1239
  .ttl-detail-list {
1251
1240
  display: grid;
@@ -1294,6 +1283,74 @@
1294
1283
  }
1295
1284
  .ttl-link:disabled { opacity: 0.5; cursor: not-allowed; }
1296
1285
  .ttl-link-danger { color: var(--dsw-alias-state-error-primary); }
1286
+ /* 表格「完成时间」列双行:时刻 + 用时 / 进行中。 */
1287
+ .ttl-td-time .ttl-time-primary { display: block; line-height: 18px; }
1288
+ .ttl-time-duration {
1289
+ display: block;
1290
+ color: var(--dsw-alias-label-tertiary);
1291
+ font-size: 11px;
1292
+ line-height: 16px;
1293
+ font-variant-numeric: tabular-nums;
1294
+ /* 完成后这一行才出现,淡入让「用时」自然接手。 */
1295
+ animation: ttl-fade-in 160ms var(--ttl-ease-out);
1296
+ }
1297
+ /* 表格行尾「⋯」与行内菜单:与看板卡片同一套动作模型。 */
1298
+ .ttl-row-more {
1299
+ width: 22px;
1300
+ height: 22px;
1301
+ padding: 0;
1302
+ border: 0;
1303
+ border-radius: 6px;
1304
+ background: none;
1305
+ color: var(--dsw-alias-label-tertiary);
1306
+ font: inherit;
1307
+ font-size: 15px;
1308
+ line-height: 1;
1309
+ cursor: pointer;
1310
+ }
1311
+ .ttl-row-more:hover {
1312
+ background: var(--dsw-alias-interactive-bg-hover);
1313
+ color: var(--dsw-alias-label-primary);
1314
+ }
1315
+ .ttl-row-more:disabled { opacity: 0.5; cursor: not-allowed; }
1316
+ /* 表格行尾:⋯ 在上,展开后的动作在下,整列右对齐且不撑破操作列。 */
1317
+ .ttl-table-more {
1318
+ display: flex;
1319
+ flex-direction: column;
1320
+ align-items: flex-end;
1321
+ gap: 2px;
1322
+ }
1323
+ .ttl-row-menu {
1324
+ display: flex;
1325
+ flex-wrap: wrap;
1326
+ justify-content: flex-end;
1327
+ gap: 0;
1328
+ animation: ttl-fade-down var(--ttl-dur-fast) var(--ttl-ease-out);
1329
+ }
1330
+ .ttl-row-menu-item {
1331
+ padding: 1px 2px;
1332
+ border: 0;
1333
+ border-radius: 6px;
1334
+ background: none;
1335
+ color: var(--dsw-alias-label-secondary);
1336
+ font: inherit;
1337
+ font-size: 12px;
1338
+ line-height: 18px;
1339
+ white-space: nowrap;
1340
+ cursor: pointer;
1341
+ }
1342
+ .ttl-row-menu-item:hover {
1343
+ background: var(--dsw-alias-interactive-bg-hover);
1344
+ color: var(--dsw-alias-label-primary);
1345
+ }
1346
+ .ttl-row-menu-item:disabled { opacity: 0.5; cursor: not-allowed; }
1347
+ .ttl-row-menu-item-danger { color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 78%, transparent); }
1348
+ .ttl-row-menu-item-danger:hover { color: var(--dsw-alias-state-error-primary); }
1349
+ .ttl-row-edit-actions { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
1350
+ /* 行内编辑时状态列改为两个紧凑选择器(可见标签隐藏,aria-label 仍在)。 */
1351
+ .ttl-edit-status { display: flex; align-items: center; gap: 4px; }
1352
+ .ttl-edit-status .ttl-select > span { display: none; }
1353
+ .ttl-edit-status .ttl-select select { max-width: 78px; font-size: 12px; }
1297
1354
  .ttl-edit-input {
1298
1355
  flex: 1;
1299
1356
  height: 30px;
@@ -1310,6 +1367,7 @@
1310
1367
  color: var(--dsw-alias-label-tertiary);
1311
1368
  font-size: 13px;
1312
1369
  line-height: 20px;
1370
+ animation: ttl-fade-in var(--ttl-dur-base) var(--ttl-ease-out);
1313
1371
  }
1314
1372
 
1315
1373
  .ttl-placeholder-title {
@@ -1607,6 +1665,8 @@
1607
1665
  height: 16px;
1608
1666
  border-radius: 8px;
1609
1667
  overflow: hidden;
1668
+ /* 时间条从左向右「画」出来,呼应创建 → 完成的时间方向。 */
1669
+ animation: ttl-bar-draw 420ms var(--ttl-ease-out) both;
1610
1670
  }
1611
1671
  .ttl-gantt-bar-completed { background: var(--dsw-alias-state-success-secondary); }
1612
1672
  .ttl-gantt-bar-ongoing {
@@ -1625,12 +1685,14 @@
1625
1685
  font-size: 10px;
1626
1686
  line-height: 1;
1627
1687
  white-space: nowrap;
1628
- }.ttl-chat {
1688
+ }
1689
+ .ttl-chat {
1629
1690
  margin-bottom: 16px;
1630
1691
  border: 0.5px solid var(--dsw-alias-border-l2);
1631
1692
  border-radius: var(--dsw-radius-md);
1632
1693
  background: var(--dsw-alias-bg-layer-1);
1633
1694
  overflow: hidden;
1695
+ animation: ttl-fade-in 160ms var(--ttl-ease-out);
1634
1696
  }
1635
1697
 
1636
1698
  .ttl-chat-headrow {
@@ -1792,6 +1854,8 @@
1792
1854
  .ttl-table-row {
1793
1855
  border-bottom: 0.5px solid var(--dsw-alias-border-l1);
1794
1856
  transition: background 0.12s ease-out;
1857
+ /* 新行 / 切视图时淡入;animation-delay 由行索引注入,形成轻错位。 */
1858
+ animation: ttl-fade-in var(--ttl-dur-base) var(--ttl-ease-out) both;
1795
1859
  }
1796
1860
  .ttl-table-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
1797
1861
  .ttl-table tbody tr:last-child { border-bottom: 0; }
@@ -1960,11 +2024,13 @@
1960
2024
  border: 0.5px solid var(--dsw-alias-border-l2);
1961
2025
  border-radius: 12px;
1962
2026
  background: var(--dsw-alias-bg-base);
1963
- transition: border-color 0.15s, box-shadow 0.15s;
2027
+ transition: border-color 0.15s, box-shadow 0.15s, transform 0.16s var(--ttl-ease-out);
2028
+ animation: ttl-rise-in 200ms var(--ttl-ease-out) backwards;
1964
2029
  }
1965
2030
  .ttl-board-card:hover {
1966
2031
  border-color: var(--dsw-alias-border-l3);
1967
2032
  box-shadow: var(--dsw-shadow-lv1);
2033
+ transform: translateY(-1px);
1968
2034
  }
1969
2035
  .ttl-board-card-editing { padding: 9px 10px; }
1970
2036
  .ttl-board-card-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
@@ -2016,6 +2082,13 @@
2016
2082
  }
2017
2083
  .ttl-check:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; }
2018
2084
  .ttl-check:disabled { opacity: 0.5; cursor: not-allowed; }
2085
+ /* 勾选:对勾从 0.55 弹到 1;只在状态切换时过渡,首屏不会逐个弹。 */
2086
+ .ttl-check svg {
2087
+ transform: scale(0.55);
2088
+ transform-origin: 50% 50%;
2089
+ transition: transform 200ms var(--ttl-ease-spring);
2090
+ }
2091
+ .ttl-check[data-checked='true'] svg { transform: scale(1); }
2019
2092
  .ttl-board-check { position: absolute; top: 8px; left: 8px; }
2020
2093
  .ttl-board-more {
2021
2094
  position: absolute;
@@ -2180,6 +2253,7 @@
2180
2253
  padding: 6px 8px;
2181
2254
  border-radius: 8px;
2182
2255
  background: var(--dsw-alias-bg-module-platform);
2256
+ animation: ttl-fade-in var(--ttl-dur-base) var(--ttl-ease-out) both;
2183
2257
  }
2184
2258
  .ttl-calendar-entry-main {
2185
2259
  display: flex;
@@ -2300,6 +2374,7 @@
2300
2374
  justify-content: center;
2301
2375
  padding: 24px;
2302
2376
  background: var(--dsw-alias-bg-mask-1);
2377
+ animation: ttl-fade-in 140ms ease-out;
2303
2378
  }
2304
2379
  .ttl-dialog {
2305
2380
  display: flex;
@@ -2311,6 +2386,7 @@
2311
2386
  background: var(--dsw-alias-bg-base);
2312
2387
  box-shadow: var(--dsw-shadow-lv3);
2313
2388
  overflow: hidden;
2389
+ animation: ttl-dialog-in 200ms var(--ttl-ease-out) both;
2314
2390
  }
2315
2391
  .ttl-dialog-head {
2316
2392
  display: flex;
@@ -2438,15 +2514,39 @@
2438
2514
  @media (max-width: 560px) {
2439
2515
  .ttl-overlay { align-items: flex-start; padding: 12px; }
2440
2516
  }
2517
+ /* ===== 动效关键帧:短、克制,只服务状态变化 ===== */
2518
+ @keyframes ttl-fade-in {
2519
+ from { opacity: 0; }
2520
+ to { opacity: 1; }
2521
+ }
2522
+ @keyframes ttl-rise-in {
2523
+ from { opacity: 0; transform: translateY(6px); }
2524
+ to { opacity: 1; transform: none; }
2525
+ }
2526
+ @keyframes ttl-fade-down {
2527
+ from { opacity: 0; transform: translateY(-4px); }
2528
+ to { opacity: 1; transform: none; }
2529
+ }
2530
+ @keyframes ttl-dialog-in {
2531
+ from { opacity: 0; transform: translateY(10px) scale(0.985); }
2532
+ to { opacity: 1; transform: none; }
2533
+ }
2534
+ @keyframes ttl-count-pop {
2535
+ from { opacity: 0.4; transform: translateY(-2px) scale(0.92); }
2536
+ to { opacity: 1; transform: none; }
2537
+ }
2538
+ @keyframes ttl-bar-draw {
2539
+ from { clip-path: inset(0 100% 0 0); }
2540
+ to { clip-path: inset(0 0 0 0); }
2541
+ }
2542
+
2441
2543
  @media (prefers-reduced-motion: reduce) {
2442
- .ttl-input,
2443
- .ttl-button,
2444
- .ttl-ghost-button,
2445
- .ttl-detail-toggle svg,
2446
- .ttl-chat-chevron,
2447
- .ttl-ghost-button-icon { transition: none; }
2448
- .ttl-skeleton-bar { animation: none; }
2449
- .ttl-toast { animation: none; }
2544
+ .ttl-root *,
2545
+ .ttl-root *::before,
2546
+ .ttl-root *::after {
2547
+ animation: none !important;
2548
+ transition: none !important;
2549
+ }
2450
2550
  }
2451
2551
  @media (max-width: 720px) {
2452
2552
  .ttl-detail-list { grid-template-columns: auto minmax(0, 1fr); }
@@ -3928,7 +4028,7 @@
3928
4028
  )
3929
4029
  );
3930
4030
  };
3931
- const renderBoardCard = (todo) => {
4031
+ const renderBoardCard = (todo, index) => {
3932
4032
  const isEditing = editingId === todo.id;
3933
4033
  const isConfirming = confirmId === todo.id;
3934
4034
  const actionsOpen = boardActionsId === todo.id;
@@ -3948,6 +4048,8 @@
3948
4048
  key: todo.id,
3949
4049
  className: isEditing ? "ttl-board-card ttl-board-card-editing" : "ttl-board-card",
3950
4050
  "data-status": todo.status,
4051
+ // 入场错位:与表格同一节奏(前 7 张,12ms 一步)。
4052
+ style: { animationDelay: `${Math.min(index || 0, 6) * 12}ms` },
3951
4053
  role: "listitem",
3952
4054
  onKeyDown: (event) => {
3953
4055
  if (!event || event.key !== "Escape") return;
@@ -4185,7 +4287,7 @@
4185
4287
  "+"
4186
4288
  ) : null
4187
4289
  ),
4188
- collapsed ? null : h("div", { className: "ttl-board-list", role: "list" }, lane.items.map(renderBoardCard))
4290
+ collapsed ? null : h("div", { className: "ttl-board-list", role: "list" }, lane.items.map((todo, index) => renderBoardCard(todo, index)))
4189
4291
  );
4190
4292
  };
4191
4293
  const renderBoardColumn = (column) => {
@@ -4235,7 +4337,7 @@
4235
4337
  tabIndex: 0,
4236
4338
  "aria-label": t("board.columnBodyAria", { name: column.label, count: total })
4237
4339
  },
4238
- lanes ? lanes.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-lanes" }, lanes.map((lane) => renderBoardLane(column, lane))) : visibleItems.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-list", role: "list" }, visibleItems.map(renderBoardCard)),
4340
+ lanes ? lanes.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-lanes" }, lanes.map((lane) => renderBoardLane(column, lane))) : visibleItems.length === 0 ? h("p", { className: "ttl-board-column-empty" }, t(emptyColumnKey(column))) : h("div", { className: "ttl-board-list", role: "list" }, visibleItems.map((todo, index) => renderBoardCard(todo, index))),
4239
4341
  !lanes && hiddenCount > 0 ? h(
4240
4342
  "button",
4241
4343
  {
@@ -4439,12 +4541,16 @@
4439
4541
 
4440
4542
  // src/client/views/table.js
4441
4543
  function createTableView(deps) {
4442
- const { confirmId, detailIdOf, direction, editingId, emptyActionKey, expandedIds, filter, handleEmptyAction, handleHeaderSort, listStatus, priorityBadge, projectInput, renderActionButtons, renderDetailContent, renderFileChips, renderRowCheck, setConfirmId, sortBy, statusBadge, titleInput, toggleExpanded, typeBadge, visibleTodos } = deps;
4443
- const pushTableRow = (rows, todo) => {
4544
+ const { busyId, cancelEdit, confirmId, detailIdOf, direction, editingId, editingPriority, editingType, emptyActionKey, expandedIds, filter, handleDelete, handleEmptyAction, handleHeaderSort, listStatus, nowMs, priorityBadge, prioritySelectControl, projectInput, renderDetailContent, renderFileChips, renderRowCheck, saveEdit, setConfirmId, setEditingPriority, setEditingType, setTableActionsId, sortBy, startEdit, statusBadge, tableActionsId, titleInput, toggleExpanded, typeBadge, typeSelectControl, visibleTodos } = deps;
4545
+ const pushTableRow = (rows, todo, index) => {
4444
4546
  const completed = todo.status === "completed";
4445
4547
  const isEditing = editingId === todo.id;
4446
4548
  const expanded = expandedIds[todo.id] === true;
4549
+ const actionsOpen = tableActionsId === todo.id;
4550
+ const isConfirming = confirmId === todo.id;
4447
4551
  const detailId = detailIdOf(todo);
4552
+ const menuId = `ttl-row-menu-${String(todo.id).replace(/[^A-Za-z0-9_-]/g, "-")}`;
4553
+ const duration = formatDurationShort(todo.createdAt, completed ? todo.completedAt : nowMs);
4448
4554
  const titleCell = isEditing ? h("td", { className: "ttl-td ttl-td-title" }, titleInput(todo)) : h(
4449
4555
  "td",
4450
4556
  { className: "ttl-td ttl-td-title" },
@@ -4473,11 +4579,15 @@
4473
4579
  key: todo.id,
4474
4580
  className: "ttl-table-row",
4475
4581
  "data-status": todo.status,
4582
+ // 入场错位:只给前 7 行加延迟(12ms 一步),长列表不用等。
4583
+ style: { animationDelay: `${Math.min(index || 0, 6) * 12}ms` },
4476
4584
  onKeyDown: (event) => {
4477
- if (event.key === "Escape" && confirmId === todo.id) {
4585
+ if (!event || event.key !== "Escape") return;
4586
+ if (isConfirming || actionsOpen) {
4478
4587
  event.preventDefault();
4479
4588
  event.stopPropagation();
4480
4589
  setConfirmId(null);
4590
+ setTableActionsId(null);
4481
4591
  }
4482
4592
  }
4483
4593
  },
@@ -4491,11 +4601,140 @@
4491
4601
  h(
4492
4602
  "td",
4493
4603
  { className: "ttl-td ttl-td-status" },
4494
- h("span", { className: "ttl-badge-row" }, statusBadge(todo), typeBadge(todo), priorityBadge(todo))
4604
+ isEditing ? h(
4605
+ "span",
4606
+ { className: "ttl-edit-status" },
4607
+ prioritySelectControl(editingPriority, setEditingPriority, busyId === todo.id),
4608
+ typeSelectControl(editingType, setEditingType, busyId === todo.id)
4609
+ ) : h("span", { className: "ttl-badge-row" }, statusBadge(todo), typeBadge(todo), priorityBadge(todo))
4495
4610
  ),
4496
4611
  h("td", { className: "ttl-td ttl-td-time" }, formatTime(todo.createdAt)),
4497
- h("td", { className: "ttl-td ttl-td-time" }, completed ? formatTime(todo.completedAt) : EMPTY_TIME),
4498
- h("td", { className: "ttl-td ttl-td-actions" }, renderActionButtons(todo, { showDetail: true, showToggle: false }))
4612
+ // 完成时间列双行:第一行时刻,第二行「用时 / 进行中」——时间账本的核心指标。
4613
+ h(
4614
+ "td",
4615
+ { className: "ttl-td ttl-td-time" },
4616
+ h("span", { className: "ttl-time-primary" }, completed ? formatTime(todo.completedAt) : EMPTY_TIME),
4617
+ duration === EMPTY_TIME ? null : h(
4618
+ "span",
4619
+ { className: "ttl-time-duration", "data-status": todo.status },
4620
+ t(completed ? "table.spent" : "table.elapsed", { duration })
4621
+ )
4622
+ ),
4623
+ h(
4624
+ "td",
4625
+ { className: "ttl-td ttl-td-actions" },
4626
+ isEditing ? h(
4627
+ "span",
4628
+ { className: "ttl-row-edit-actions" },
4629
+ h(
4630
+ "button",
4631
+ {
4632
+ type: "button",
4633
+ className: "ttl-link",
4634
+ disabled: busyId === todo.id,
4635
+ onClick: () => {
4636
+ void saveEdit(todo);
4637
+ }
4638
+ },
4639
+ t("row.save")
4640
+ ),
4641
+ h(
4642
+ "button",
4643
+ { type: "button", className: "ttl-link", disabled: busyId === todo.id, onClick: cancelEdit },
4644
+ t("row.cancel")
4645
+ )
4646
+ ) : h(
4647
+ "div",
4648
+ { className: "ttl-table-more" },
4649
+ h(
4650
+ "button",
4651
+ {
4652
+ type: "button",
4653
+ className: "ttl-row-more",
4654
+ "aria-expanded": actionsOpen ? "true" : "false",
4655
+ "aria-controls": actionsOpen ? menuId : void 0,
4656
+ "aria-label": t("table.moreAria", { title: todo.title === "" ? t("list.empty") : todo.title }),
4657
+ title: t("table.more"),
4658
+ disabled: busyId === todo.id,
4659
+ onClick: () => {
4660
+ setConfirmId(null);
4661
+ setTableActionsId(actionsOpen ? null : todo.id);
4662
+ }
4663
+ },
4664
+ "⋯"
4665
+ ),
4666
+ actionsOpen ? h(
4667
+ "div",
4668
+ { id: menuId, className: "ttl-row-menu", role: "group", "aria-label": t("table.more") },
4669
+ isConfirming ? [
4670
+ h(
4671
+ "button",
4672
+ {
4673
+ key: "confirm",
4674
+ type: "button",
4675
+ className: "ttl-row-menu-item ttl-row-menu-item-danger",
4676
+ disabled: busyId === todo.id,
4677
+ onClick: () => {
4678
+ void handleDelete(todo);
4679
+ }
4680
+ },
4681
+ t("row.confirmDelete")
4682
+ ),
4683
+ h(
4684
+ "button",
4685
+ {
4686
+ key: "cancel",
4687
+ type: "button",
4688
+ className: "ttl-row-menu-item",
4689
+ onClick: () => setConfirmId(null)
4690
+ },
4691
+ t("row.cancel")
4692
+ )
4693
+ ] : [
4694
+ h(
4695
+ "button",
4696
+ {
4697
+ key: "detail",
4698
+ type: "button",
4699
+ className: "ttl-row-menu-item",
4700
+ "aria-expanded": expanded ? "true" : "false",
4701
+ "aria-controls": detailId,
4702
+ onClick: () => {
4703
+ setTableActionsId(null);
4704
+ toggleExpanded(todo);
4705
+ }
4706
+ },
4707
+ t("table.detail")
4708
+ ),
4709
+ h(
4710
+ "button",
4711
+ {
4712
+ key: "edit",
4713
+ type: "button",
4714
+ className: "ttl-row-menu-item",
4715
+ disabled: busyId === todo.id,
4716
+ onClick: () => {
4717
+ setTableActionsId(null);
4718
+ startEdit(todo);
4719
+ }
4720
+ },
4721
+ t("row.edit")
4722
+ ),
4723
+ h(
4724
+ "button",
4725
+ {
4726
+ key: "delete",
4727
+ type: "button",
4728
+ className: "ttl-row-menu-item ttl-row-menu-item-danger",
4729
+ disabled: busyId === todo.id,
4730
+ onClick: () => setConfirmId(todo.id)
4731
+ },
4732
+ t("row.delete")
4733
+ )
4734
+ ]
4735
+ ) : null
4736
+ )
4737
+ )
4499
4738
  )
4500
4739
  );
4501
4740
  if (expanded) {
@@ -4543,10 +4782,10 @@
4543
4782
  )
4544
4783
  )
4545
4784
  );
4546
- for (const todo of group.items) pushTableRow(rows, todo);
4785
+ for (const [index, todo] of group.items.entries()) pushTableRow(rows, todo, index);
4547
4786
  }
4548
4787
  } else {
4549
- for (const todo of visibleTodos) pushTableRow(rows, todo);
4788
+ for (const [index, todo] of visibleTodos.entries()) pushTableRow(rows, todo, index);
4550
4789
  }
4551
4790
  return h(
4552
4791
  "div",
@@ -4640,6 +4879,7 @@
4640
4879
  open,
4641
4880
  busy,
4642
4881
  applying,
4882
+ dirty,
4643
4883
  fileName,
4644
4884
  mode,
4645
4885
  preview,
@@ -4660,7 +4900,9 @@
4660
4900
  const rowCount = record !== null && typeof record.rowCount === "number" ? record.rowCount : 0;
4661
4901
  const activeMode = TRANSFER_MODES.find((option) => option.value === mode) ?? TRANSFER_MODES[0];
4662
4902
  const canConfirm = summary !== null && busy !== true && applying !== true;
4903
+ const canDismissBackdrop = dirty !== true && busy !== true && applying !== true;
4663
4904
  const backdrop = (event) => {
4905
+ if (!canDismissBackdrop) return;
4664
4906
  if (event && event.target === event.currentTarget) onClose();
4665
4907
  };
4666
4908
  return h(
@@ -4676,7 +4918,7 @@
4676
4918
  onKeyDown: (event) => {
4677
4919
  if (event && event.key === "Escape") {
4678
4920
  event.stopPropagation();
4679
- onClose();
4921
+ if (canDismissBackdrop) onClose();
4680
4922
  }
4681
4923
  }
4682
4924
  },
@@ -4887,6 +5129,7 @@
4887
5129
  const [messages, setMessages] = React.useState(() => [{ id: 0, role: "assistant", welcome: true }]);
4888
5130
  const [boardGroupBy, setBoardGroupBy] = React.useState("status");
4889
5131
  const [boardActionsId, setBoardActionsId] = React.useState(null);
5132
+ const [tableActionsId, setTableActionsId] = React.useState(null);
4890
5133
  const [boardShowAllCompleted, setBoardShowAllCompleted] = React.useState(false);
4891
5134
  const [search, setSearch] = React.useState("");
4892
5135
  const [boardLaneCollapsed, setBoardLaneCollapsed] = React.useState({});
@@ -5244,6 +5487,30 @@
5244
5487
  cancelEdit();
5245
5488
  }
5246
5489
  };
5490
+ const restoreDeleted = async (todo) => {
5491
+ if (todo === null || todo === void 0) return;
5492
+ try {
5493
+ const content = JSON.stringify([{
5494
+ id: todo.id,
5495
+ title: todo.title,
5496
+ project: todo.project,
5497
+ files: normalizeFiles(todo.files),
5498
+ status: todo.status,
5499
+ priority: todo.priority,
5500
+ type: todo.type,
5501
+ createdAt: todo.createdAt,
5502
+ completedAt: todo.completedAt,
5503
+ updatedAt: todo.updatedAt
5504
+ }]);
5505
+ await request("/import", { content, mode: "merge" });
5506
+ if (!mountedRef.current) return;
5507
+ showNotice("success", t("notice.restored"));
5508
+ void fetchProjectSuggestions(workspaceRef.current, true);
5509
+ await reload(true);
5510
+ } catch (error) {
5511
+ showNotice("error", messageOf(error));
5512
+ }
5513
+ };
5247
5514
  const handleDelete = async (todo) => {
5248
5515
  if (todo === null || todo === void 0 || busyId !== null) return;
5249
5516
  setBusyId(todo.id);
@@ -5251,7 +5518,12 @@
5251
5518
  await request("/delete", { id: todo.id });
5252
5519
  if (!mountedRef.current) return;
5253
5520
  setConfirmId(null);
5254
- showNotice("success", t("notice.deleted"));
5521
+ setTableActionsId(null);
5522
+ showNotice("success", t("notice.deleted"), {
5523
+ onClick: () => {
5524
+ void restoreDeleted(todo);
5525
+ }
5526
+ });
5255
5527
  void fetchProjectSuggestions(workspaceRef.current, true);
5256
5528
  await reload(true);
5257
5529
  } catch (error) {
@@ -6197,6 +6469,7 @@
6197
6469
  calendarMonth,
6198
6470
  calendarSelectedDay,
6199
6471
  calendarYear,
6472
+ cancelEdit,
6200
6473
  confirmId,
6201
6474
  density,
6202
6475
  detailIdOf,
@@ -6230,6 +6503,7 @@
6230
6503
  renderDetailContent,
6231
6504
  renderFileChips,
6232
6505
  renderRowCheck,
6506
+ saveEdit,
6233
6507
  setBoardActionsId,
6234
6508
  setBoardLaneCollapsed,
6235
6509
  setBoardShowAllCompleted,
@@ -6237,9 +6511,11 @@
6237
6511
  setConfirmId,
6238
6512
  setEditingPriority,
6239
6513
  setEditingType,
6514
+ setTableActionsId,
6240
6515
  sortBy,
6241
6516
  startEdit,
6242
6517
  statusBadge,
6518
+ tableActionsId,
6243
6519
  titleInput,
6244
6520
  toggleExpanded,
6245
6521
  toggleGanttExpanded,
@@ -6299,38 +6575,10 @@
6299
6575
  onClick: () => setFilter(entry.value)
6300
6576
  },
6301
6577
  t(entry.labelKey),
6302
- h("span", { className: "ttl-seg-count" }, String(filterCounts[entry.value] || 0))
6578
+ h("span", { key: filterCounts[entry.value] || 0, className: "ttl-seg-count" }, String(filterCounts[entry.value] || 0))
6303
6579
  )
6304
6580
  )
6305
6581
  );
6306
- const sortSelect = h(
6307
- "label",
6308
- { className: "ttl-select" },
6309
- h("span", null, t("sort.label")),
6310
- h(
6311
- "select",
6312
- {
6313
- value: sortBy,
6314
- "aria-label": t("sort.label"),
6315
- onChange: (event) => setSortBy(event.target.value)
6316
- },
6317
- SORTS.map(
6318
- (entry) => h("option", { key: entry.value, value: entry.value }, t(entry.labelKey))
6319
- )
6320
- )
6321
- );
6322
- const directionLabel = t(direction === "asc" ? "direction.asc" : "direction.desc");
6323
- const directionToggle = h(
6324
- "button",
6325
- {
6326
- type: "button",
6327
- className: "ttl-sort-dir",
6328
- "aria-label": t("direction.toggleAria", { direction: directionLabel }),
6329
- title: t("direction.toggleAria", { direction: directionLabel }),
6330
- onClick: () => setDirection(direction === "asc" ? "desc" : "asc")
6331
- },
6332
- h("span", { "aria-hidden": "true" }, direction === "asc" ? "↑" : "↓")
6333
- );
6334
6582
  const viewToggle = h(
6335
6583
  "div",
6336
6584
  { className: "ttl-seg", role: "group", "aria-label": t("view.aria") },
@@ -6575,41 +6823,38 @@
6575
6823
  h("path", { d: "M8 2v7m0 0l3-3m-3 3L5 6M3 12v1.5a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5V12" })
6576
6824
  ),
6577
6825
  t("transfer.open")
6578
- ),
6579
- h(
6580
- "button",
6581
- {
6582
- type: "button",
6583
- className: "ttl-ghost-button",
6584
- "aria-expanded": chatOpen ? "true" : "false",
6585
- "aria-controls": chatOpen ? "ttl-chat-panel" : void 0,
6586
- "aria-label": chatOpen ? t("chat.collapse") : t("chat.expand"),
6587
- title: chatOpen ? t("chat.collapse") : t("chat.expand"),
6588
- onClick: () => setChatOpen((value) => !value)
6589
- },
6590
- h(
6591
- "svg",
6592
- {
6593
- className: "ttl-ghost-button-icon",
6594
- width: 12,
6595
- height: 12,
6596
- viewBox: "0 0 16 16",
6597
- fill: "none",
6598
- stroke: "currentColor",
6599
- strokeWidth: 1.4,
6600
- strokeLinecap: "round",
6601
- strokeLinejoin: "round",
6602
- "aria-hidden": "true",
6603
- focusable: "false"
6604
- },
6605
- h("path", { d: chatOpen ? "M4 10l4-4 4 4" : "M4 6l4 4-4 4" })
6606
- ),
6607
- t("chat.title")
6608
6826
  )
6609
6827
  )
6610
6828
  ),
6611
- // 新建待办:整页唯一的主操作区。
6612
- h(
6829
+ // 捕获区:单条与批量共用同一位置;批量展开时不在列表上方再叠一层。
6830
+ chatOpen ? h(ChatPanel, {
6831
+ id: "ttl-chat-panel",
6832
+ open: true,
6833
+ busy: chatBusy,
6834
+ messages,
6835
+ input: chatInput,
6836
+ suggestions: fileSuggestions,
6837
+ suggestionsKind: suggestKind,
6838
+ workspaces,
6839
+ workspace,
6840
+ onWorkspaceChange: handleWorkspaceChange,
6841
+ suggestionsOpen: suggestOpen && suggestTarget === "chat",
6842
+ suggestionsLoading: suggestLoading,
6843
+ suggestionsError: suggestError,
6844
+ suggestionsActive: suggestActive,
6845
+ onToggle: () => setChatOpen(false),
6846
+ onChange: (value, caret) => handleChatInputChange(value, caret),
6847
+ onSend: () => {
6848
+ void handleChatSend();
6849
+ },
6850
+ onUndo: (message) => {
6851
+ void handleChatUndo(message);
6852
+ },
6853
+ onSuggestMove: moveFileSuggest,
6854
+ onSuggestClose: closeFileSuggest,
6855
+ onSuggestSelect: selectSuggestion,
6856
+ onCopyFile: copyFilePath
6857
+ }) : h(
6613
6858
  "form",
6614
6859
  { className: "ttl-composer", onSubmit: handleCreate, "aria-label": t("create.aria") },
6615
6860
  h("input", {
@@ -6656,6 +6901,18 @@
6656
6901
  }),
6657
6902
  prioritySelectControl(newPriority, setNewPriority, creating),
6658
6903
  typeSelectControl(newType, setNewType, creating),
6904
+ h(
6905
+ "button",
6906
+ {
6907
+ type: "button",
6908
+ className: "ttl-ghost-button ttl-composer-batch",
6909
+ title: t("create.batchTitle"),
6910
+ "aria-expanded": "false",
6911
+ "aria-controls": "ttl-chat-panel",
6912
+ onClick: () => setChatOpen(true)
6913
+ },
6914
+ t("create.batch")
6915
+ ),
6659
6916
  h(
6660
6917
  "button",
6661
6918
  {
@@ -6666,35 +6923,6 @@
6666
6923
  creating ? t("create.actionBusy") : t("create.action")
6667
6924
  )
6668
6925
  ),
6669
- // 对话录入:按需展开的次要录入方式,默认收起,不与新建抢注意力。
6670
- chatOpen ? h(ChatPanel, {
6671
- id: "ttl-chat-panel",
6672
- open: true,
6673
- busy: chatBusy,
6674
- messages,
6675
- input: chatInput,
6676
- suggestions: fileSuggestions,
6677
- suggestionsKind: suggestKind,
6678
- workspaces,
6679
- workspace,
6680
- onWorkspaceChange: handleWorkspaceChange,
6681
- suggestionsOpen: suggestOpen && suggestTarget === "chat",
6682
- suggestionsLoading: suggestLoading,
6683
- suggestionsError: suggestError,
6684
- suggestionsActive: suggestActive,
6685
- onToggle: () => setChatOpen((value) => !value),
6686
- onChange: (value, caret) => handleChatInputChange(value, caret),
6687
- onSend: () => {
6688
- void handleChatSend();
6689
- },
6690
- onUndo: (message) => {
6691
- void handleChatUndo(message);
6692
- },
6693
- onSuggestMove: moveFileSuggest,
6694
- onSuggestClose: closeFileSuggest,
6695
- onSuggestSelect: selectSuggestion,
6696
- onCopyFile: copyFilePath
6697
- }) : null,
6698
6926
  // 数据区:视图切换与列表同属一张卡片。
6699
6927
  h(
6700
6928
  "div",
@@ -6702,26 +6930,32 @@
6702
6930
  h(
6703
6931
  "div",
6704
6932
  { className: "ttl-toolbar" },
6933
+ // 第一行固定:视图切换 + 搜索。切视图时控件位置不变。
6705
6934
  h(
6706
6935
  "div",
6707
- { className: "ttl-group" },
6708
- viewToggle,
6709
- view === "table" ? filterTabs : view === "board" ? h(React.Fragment, null, boardGroupSelect, densitySelect, filterTabs) : view === "gantt" ? rangeSelect : calendarBasisSelect
6936
+ { className: "ttl-toolbar-row" },
6937
+ h("div", { className: "ttl-group" }, viewToggle),
6938
+ h(
6939
+ "div",
6940
+ { className: "ttl-group" },
6941
+ searchInput,
6942
+ h(
6943
+ "span",
6944
+ { className: "ttl-refreshing", "aria-live": "polite" },
6945
+ refreshing && listStatus !== "loading" ? t("list.refreshing") : ""
6946
+ )
6947
+ )
6710
6948
  ),
6949
+ // 第二行:左侧是当前视图专属控件,右侧始终是分类 / 类型筛选。
6711
6950
  h(
6712
6951
  "div",
6713
- { className: "ttl-group" },
6714
- searchInput,
6715
- projectFilterSelect,
6716
- typeFilterSelect,
6717
- view === "table" ? sortSelect : null,
6718
- view === "table" ? directionToggle : null,
6719
- view === "calendar" ? calendarNav : null,
6952
+ { className: "ttl-toolbar-row" },
6720
6953
  h(
6721
- "span",
6722
- { className: "ttl-refreshing", "aria-live": "polite" },
6723
- refreshing && listStatus !== "loading" ? t("list.refreshing") : ""
6724
- )
6954
+ "div",
6955
+ { className: "ttl-group" },
6956
+ view === "table" ? filterTabs : view === "board" ? h(React.Fragment, null, boardGroupSelect, densitySelect, filterTabs) : view === "gantt" ? rangeSelect : h(React.Fragment, null, calendarBasisSelect, calendarNav)
6957
+ ),
6958
+ h("div", { className: "ttl-group" }, projectFilterSelect, typeFilterSelect)
6725
6959
  )
6726
6960
  ),
6727
6961
  h("div", { className: "ttl-list" }, listBody)
@@ -6763,6 +6997,8 @@
6763
6997
  open: transferOpen,
6764
6998
  busy: transferBusy,
6765
6999
  applying: transferApplying,
7000
+ // 有文件 / 预览 / 改动过策略时,遮罩与 Esc 不再直接关闭(防误丢导入内容)。
7001
+ dirty: transferFileName !== "" || transferContent !== "" || transferPreview !== null,
6766
7002
  fileName: transferFileName,
6767
7003
  mode: transferMode,
6768
7004
  preview: transferPreview,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-todo-time-log",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "DeepSeek Harness 待办插件:自动记录每条待办的创建与完成时间,四视图(表格 / 看板 / 甘特图 / 日历)呈现真实耗时(Todo time log for DeepSeek Harness)",
5
5
  "keywords": [
6
6
  "dsh",