dsh-global-todo 0.5.2 → 0.6.0
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/CHANGELOG.md +37 -0
- package/docs/screenshots/panel-detail.png +0 -0
- package/docs/screenshots/panel-select.png +0 -0
- package/docs/screenshots/panel.png +0 -0
- package/lib/client.js +518 -164
- package/lib/index.js +11 -2
- package/lib/reminders.js +3 -1
- package/lib/routes.js +2 -0
- package/lib/settings.js +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
## [0.6.0] — 2026-10-20
|
|
2
|
+
|
|
3
|
+
A design pass over the whole panel: one list, fewer competing controls, one type scale.
|
|
4
|
+
|
|
5
|
+
- **The layout is a column, not a wrapping row.** `.gt-root`/`.gt-inner` gained `min-width: 0` so the
|
|
6
|
+
column shrinks with its slot instead of overflowing at 1280 and 1100; the header wraps its actions
|
|
7
|
+
and the toolbar is two semantic rows (filter/search above, context above the list). The working
|
|
8
|
+
directory, handoff target and browse controls move behind a single Context toggle, collapsed by
|
|
9
|
+
default.
|
|
10
|
+
- **Five type sizes and one weight scale.** 20/28 panel title, 14/22 row title, 13/20 controls and
|
|
11
|
+
body, 12/18 metadata and section labels, 11.5 mono for paths. Buttons, inputs and tabs share a
|
|
12
|
+
28px control height; the settings card is a 340px floating inspector (fixed, mutually exclusive
|
|
13
|
+
with the detail card) instead of an in-flow block that pushed the list down.
|
|
14
|
+
- **Rows read top-down.** The title opens the detail (the kebab remains as a redundant door); the
|
|
15
|
+
dispatch button is a quiet action instead of an outlined primary; metadata loses the origin path
|
|
16
|
+
and the stray separator (CSS-generated dots), and tags cap at four. Group-level priority hides
|
|
17
|
+
while the group is expanded, where every row already shows it.
|
|
18
|
+
- **Copy and states.** Full-width colons no longer leak into the English UI, `1 items` is fixed,
|
|
19
|
+
priority chips say Urgent/High/Normal/Low instead of P-codes, the empty state has a primary
|
|
20
|
+
action, the error banner can be dismissed, and the reminder bar keeps its accent on a 2px edge
|
|
21
|
+
(amber text failed contrast). The footer's store path and version move into Settings → About.
|
|
22
|
+
|
|
23
|
+
## [0.5.3] — 2026-10-20
|
|
24
|
+
|
|
25
|
+
Settings a person can reach, and reminders that behave like reminders.
|
|
26
|
+
|
|
27
|
+
- **A settings popover.** Reminders and the external ingest door were both API-only; the header
|
|
28
|
+
now opens a small panel for the reminder switch, the lead time, the ingest switch, the token
|
|
29
|
+
(generate a fresh one in place; leave blank to keep the current) and the inbox confidence
|
|
30
|
+
threshold. The token still never comes back from the host.
|
|
31
|
+
- **Reminders gained a lead time and a switch.** remindersEnabled and reminderLeadMinutes live in
|
|
32
|
+
settings; the host sweep reads them each tick, announces at dueAt minus lead, and clears the due
|
|
33
|
+
list when reminders are off.
|
|
34
|
+
- **Browser notifications are opt-in.** When the panel is open and a new deadline becomes due, it
|
|
35
|
+
fires a system notification if permission was granted; otherwise the due bar offers to ask for
|
|
36
|
+
permission. The notification is a bonus — the bar is the source of truth.
|
|
37
|
+
|
|
1
38
|
# Changelog
|
|
2
39
|
|
|
3
40
|
All notable changes to this project are documented in this file.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -50,7 +50,7 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
* different mechanisms (restart vs. hot reload), so "which one am I looking
|
|
51
51
|
* at" needs an answer that the bundle itself carries.
|
|
52
52
|
*/
|
|
53
|
-
const UI_REV = '
|
|
53
|
+
const UI_REV = 'ui36'
|
|
54
54
|
/** Last language resolved for the shell; module-level so a fetch failure can be worded too. */
|
|
55
55
|
let activeLang = 'zh'
|
|
56
56
|
|
|
@@ -64,6 +64,7 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
'action.retry': '重试',
|
|
65
65
|
'action.add': '添加',
|
|
66
66
|
'action.more': '更多字段',
|
|
67
|
+
'action.overflow': '更多操作',
|
|
67
68
|
'action.less': '收起字段',
|
|
68
69
|
'action.detail': '详情',
|
|
69
70
|
'action.hide': '收起',
|
|
@@ -90,6 +91,21 @@ window.__ModuleLoader__.load({
|
|
|
90
91
|
'action.import': '导入',
|
|
91
92
|
'action.exportHint': '把整份清单导出成 JSON 文件(就是存储用的那份文档)',
|
|
92
93
|
'action.importHint': '从 JSON 文件导入:同 id 覆盖、新 id 追加,不删除现有待办',
|
|
94
|
+
'settings.title': '设置',
|
|
95
|
+
'settings.context': '上下文',
|
|
96
|
+
'settings.reminders': '到点提醒',
|
|
97
|
+
'settings.remindersEnable': '开启到点提醒',
|
|
98
|
+
'settings.lead': '提前(分钟)',
|
|
99
|
+
'settings.ingest': '外部接入(ingest)',
|
|
100
|
+
'settings.ingestEnable': '启用外部写入',
|
|
101
|
+
'settings.token': '访问 token',
|
|
102
|
+
'settings.tokenSet': '已设置,留空保持不变',
|
|
103
|
+
'settings.generate': '生成新 token',
|
|
104
|
+
'settings.confirmBelow': '低于此置信度进收件箱',
|
|
105
|
+
'settings.save': '保存',
|
|
106
|
+
'settings.cancel': '取消',
|
|
107
|
+
'settings.saved': '设置已保存',
|
|
108
|
+
'settings.about': '关于',
|
|
93
109
|
'notice.exported': '已导出 {n} 条待办',
|
|
94
110
|
'notice.imported': '已导入 {n} 条(合并,现有共 {total} 条)',
|
|
95
111
|
'import.badJson': '这个文件不是合法的 JSON',
|
|
@@ -111,7 +127,7 @@ window.__ModuleLoader__.load({
|
|
|
111
127
|
'notice.bulk': '已更新 {n} 条',
|
|
112
128
|
'notice.bulkPartial': '已更新 {n} 条,跳过 {m} 条',
|
|
113
129
|
'select.clear': '清除',
|
|
114
|
-
'select.done': '
|
|
130
|
+
'select.done': '退出选择',
|
|
115
131
|
'select.hint': '先勾几条待办,再点「一键分配」:优先回到写下它的那个对话,其次同工作区的对话,都找不到才让你选。',
|
|
116
132
|
'assign.working': '正在分配…',
|
|
117
133
|
'assign.summary': '已分配 {n} 条(原对话 {origin} · 同工作区 {workspace} · 你选的 {chosen})',
|
|
@@ -138,6 +154,8 @@ window.__ModuleLoader__.load({
|
|
|
138
154
|
'notice.externalChange': '清单在别处被改过,已载入最新内容',
|
|
139
155
|
'reminder.title': '到点提醒({n})',
|
|
140
156
|
'reminder.dismiss': '知道了',
|
|
157
|
+
'reminder.enableNotifications': '开启系统通知',
|
|
158
|
+
'reminder.notifyTitle': '待办到点',
|
|
141
159
|
'import.preview': '将导入 {total} 条:新增 {create}、更新 {update}',
|
|
142
160
|
'import.confirm': '导入',
|
|
143
161
|
'meta.seqHint': '给人看的编号',
|
|
@@ -203,10 +221,10 @@ window.__ModuleLoader__.load({
|
|
|
203
221
|
'field.linkValue': '文档绝对路径或网址',
|
|
204
222
|
'field.linkTitle': '给它起个短名字(可空)',
|
|
205
223
|
'field.search': '搜标题、描述、标签、附件…',
|
|
206
|
-
'priority.low': '
|
|
207
|
-
'priority.normal': '
|
|
208
|
-
'priority.high': '
|
|
209
|
-
'priority.urgent': '
|
|
224
|
+
'priority.low': '低',
|
|
225
|
+
'priority.normal': '常规',
|
|
226
|
+
'priority.high': '高',
|
|
227
|
+
'priority.urgent': '紧急',
|
|
210
228
|
'group.by': '分组',
|
|
211
229
|
'group.conversation': '对话',
|
|
212
230
|
'group.time': '时间',
|
|
@@ -214,6 +232,7 @@ window.__ModuleLoader__.load({
|
|
|
214
232
|
'group.manual': '手动添加',
|
|
215
233
|
'group.other': '其他',
|
|
216
234
|
'group.items': '{n} 条',
|
|
235
|
+
'group.item': '1 条',
|
|
217
236
|
'group.thisConversation': '本对话',
|
|
218
237
|
'group.overdue': '逾期',
|
|
219
238
|
'group.today': '今天',
|
|
@@ -223,6 +242,7 @@ window.__ModuleLoader__.load({
|
|
|
223
242
|
'group.done': '已完成',
|
|
224
243
|
'group.cancelled': '已取消',
|
|
225
244
|
'empty.none': '还没有待办。上面写一句,回车就记下了。',
|
|
245
|
+
'empty.add': '记第一条',
|
|
226
246
|
'empty.filtered': '这个筛选下没有待办。',
|
|
227
247
|
'meta.added': '加入于',
|
|
228
248
|
'meta.due': '截止',
|
|
@@ -285,6 +305,7 @@ window.__ModuleLoader__.load({
|
|
|
285
305
|
'action.retry': 'Retry',
|
|
286
306
|
'action.add': 'Add',
|
|
287
307
|
'action.more': 'More fields',
|
|
308
|
+
'action.overflow': 'More actions',
|
|
288
309
|
'action.less': 'Fewer fields',
|
|
289
310
|
'action.detail': 'Details',
|
|
290
311
|
'action.hide': 'Hide',
|
|
@@ -311,6 +332,21 @@ window.__ModuleLoader__.load({
|
|
|
311
332
|
'action.import': 'Import',
|
|
312
333
|
'action.exportHint': 'Download the whole list as JSON (the document the store keeps)',
|
|
313
334
|
'action.importHint': 'Import a JSON file: same ids are updated, new ids appended, nothing deleted',
|
|
335
|
+
'settings.title': 'Settings',
|
|
336
|
+
'settings.context': 'Context',
|
|
337
|
+
'settings.reminders': 'Due reminders',
|
|
338
|
+
'settings.remindersEnable': 'Announce deadlines',
|
|
339
|
+
'settings.lead': 'Lead time (min)',
|
|
340
|
+
'settings.ingest': 'External ingest',
|
|
341
|
+
'settings.ingestEnable': 'Accept external writes',
|
|
342
|
+
'settings.token': 'Access token',
|
|
343
|
+
'settings.tokenSet': 'Set — leave blank to keep',
|
|
344
|
+
'settings.generate': 'New token',
|
|
345
|
+
'settings.confirmBelow': 'Below this confidence → inbox',
|
|
346
|
+
'settings.save': 'Save',
|
|
347
|
+
'settings.cancel': 'Cancel',
|
|
348
|
+
'settings.saved': 'Settings saved',
|
|
349
|
+
'settings.about': 'About',
|
|
314
350
|
'notice.exported': 'Exported {n} todos',
|
|
315
351
|
'notice.imported': 'Imported {n} (merged; {total} in the list now)',
|
|
316
352
|
'import.badJson': 'That file is not valid JSON',
|
|
@@ -359,6 +395,8 @@ window.__ModuleLoader__.load({
|
|
|
359
395
|
'notice.externalChange': 'The list changed somewhere else; the latest content is loaded',
|
|
360
396
|
'reminder.title': 'Due now ({n})',
|
|
361
397
|
'reminder.dismiss': 'Dismiss',
|
|
398
|
+
'reminder.enableNotifications': 'Enable notifications',
|
|
399
|
+
'reminder.notifyTitle': 'Todo due',
|
|
362
400
|
'import.preview': 'Will import {total}: {create} new, {update} updated',
|
|
363
401
|
'import.confirm': 'Import',
|
|
364
402
|
'meta.seqHint': 'Human-facing number',
|
|
@@ -426,16 +464,17 @@ window.__ModuleLoader__.load({
|
|
|
426
464
|
'field.linkValue': 'Absolute document path or URL',
|
|
427
465
|
'field.linkTitle': 'Short label (optional)',
|
|
428
466
|
'field.search': 'Search title, description, tags, attachments…',
|
|
429
|
-
'priority.low': '
|
|
430
|
-
'priority.normal': '
|
|
431
|
-
'priority.high': '
|
|
432
|
-
'priority.urgent': '
|
|
467
|
+
'priority.low': 'Low',
|
|
468
|
+
'priority.normal': 'Normal',
|
|
469
|
+
'priority.high': 'High',
|
|
470
|
+
'priority.urgent': 'Urgent',
|
|
433
471
|
'group.by': 'Group',
|
|
434
472
|
'group.time': 'Time',
|
|
435
473
|
'group.level': 'Level',
|
|
436
474
|
'group.manual': 'Added by hand',
|
|
437
475
|
'group.other': 'Other',
|
|
438
476
|
'group.items': '{n} items',
|
|
477
|
+
'group.item': '1 item',
|
|
439
478
|
'group.thisConversation': 'this conversation',
|
|
440
479
|
'group.overdue': 'Overdue',
|
|
441
480
|
'group.today': 'Today',
|
|
@@ -445,6 +484,7 @@ window.__ModuleLoader__.load({
|
|
|
445
484
|
'group.done': 'Done',
|
|
446
485
|
'group.cancelled': 'Cancelled',
|
|
447
486
|
'empty.none': 'No todos yet. Write one above and press Enter.',
|
|
487
|
+
'empty.add': 'Add your first todo',
|
|
448
488
|
'empty.filtered': 'Nothing matches this filter.',
|
|
449
489
|
'meta.added': 'added',
|
|
450
490
|
'meta.due': 'due',
|
|
@@ -841,6 +881,17 @@ window.__ModuleLoader__.load({
|
|
|
841
881
|
)
|
|
842
882
|
}
|
|
843
883
|
if (props.name === 'clock') return h('svg', common, h('circle', { cx: 8, cy: 8, r: 5.6 }), h('path', { d: 'M8 4.8V8l2.2 1.4' }))
|
|
884
|
+
if (props.name === 'close') return h('svg', common, h('path', { d: 'M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6' }))
|
|
885
|
+
if (props.name === 'gear') {
|
|
886
|
+
return h(
|
|
887
|
+
'svg',
|
|
888
|
+
common,
|
|
889
|
+
h('path', { d: 'M2.6 4.4h10.8M2.6 8h10.8M2.6 11.6h10.8' }),
|
|
890
|
+
h('circle', { cx: 6, cy: 4.4, r: 1.5, fill: 'currentColor', stroke: 'none' }),
|
|
891
|
+
h('circle', { cx: 10, cy: 8, r: 1.5, fill: 'currentColor', stroke: 'none' }),
|
|
892
|
+
h('circle', { cx: 5.2, cy: 11.6, r: 1.5, fill: 'currentColor', stroke: 'none' }),
|
|
893
|
+
)
|
|
894
|
+
}
|
|
844
895
|
if (props.name === 'chevron') return h('svg', common, h('path', { d: 'M5.6 3.8L10 8l-4.4 4.2' }))
|
|
845
896
|
if (props.name === 'flag') return h('svg', common, h('path', { d: 'M4 14V2.6h7l-1.2 2.8L11 8.2H4' }))
|
|
846
897
|
if (props.name === 'mute') {
|
|
@@ -857,25 +908,29 @@ window.__ModuleLoader__.load({
|
|
|
857
908
|
// ------------------------------------------------------------------ 样式
|
|
858
909
|
|
|
859
910
|
const CSS = `
|
|
860
|
-
.gt-root { display: flex; flex-direction: column; height: 100%; min-height: 0;
|
|
861
|
-
color: var(--dsw-alias-label-primary); font-size: 13px; }
|
|
862
|
-
.gt-inner { display: flex; flex-direction: column; gap:
|
|
863
|
-
margin: 0 auto; padding:
|
|
864
|
-
.gt-head { display: flex; align-items:
|
|
911
|
+
.gt-root { display: flex; flex-direction: column; height: 100%; min-height: 0; min-width: 0;
|
|
912
|
+
color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.5; }
|
|
913
|
+
.gt-inner { display: flex; flex-direction: column; gap: 14px; width: 100%; max-width: 1040px; min-width: 0;
|
|
914
|
+
box-sizing: border-box; margin: 0 auto; padding: 20px 24px 32px; flex: 1; }
|
|
915
|
+
.gt-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
916
|
+
.gt-head-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; flex: 0 0 auto; }
|
|
865
917
|
.gt-headline { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
866
|
-
.gt-title { font-size:
|
|
867
|
-
.gt-sub { color: var(--dsw-alias-label-secondary); font-size:
|
|
918
|
+
.gt-title { font-size: 20px; line-height: 28px; font-weight: 600; letter-spacing: 0; }
|
|
919
|
+
.gt-sub { color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; }
|
|
868
920
|
.gt-spacer { flex: 1; }
|
|
869
921
|
.gt-iconbtn { display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
|
870
922
|
background: transparent; color: var(--dsw-alias-label-secondary); border: 1px solid transparent;
|
|
871
|
-
border-radius: 8px; padding:
|
|
923
|
+
border-radius: 8px; padding: 0 7px; min-height: 28px; min-width: 28px; box-sizing: border-box;
|
|
924
|
+
font: inherit; cursor: pointer; }
|
|
872
925
|
.gt-iconbtn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l1);
|
|
873
926
|
background: var(--dsw-alias-bg-layer-1); }
|
|
874
|
-
.gt-btn { display: inline-flex; align-items: center; gap:
|
|
927
|
+
.gt-btn { display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-1);
|
|
875
928
|
color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px;
|
|
876
|
-
padding:
|
|
929
|
+
padding: 0 11px; min-height: 28px; box-sizing: border-box; font: inherit; font-size: 13px;
|
|
930
|
+
font-weight: 500; line-height: 20px; cursor: pointer; white-space: nowrap; }
|
|
877
931
|
.gt-btn:hover { border-color: var(--dsw-alias-border-l2); }
|
|
878
|
-
.gt-btn:disabled { opacity:
|
|
932
|
+
.gt-btn:disabled { opacity: 1; background: var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary);
|
|
933
|
+
border-color: transparent; cursor: default; }
|
|
879
934
|
.gt-btn-primary { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary);
|
|
880
935
|
color: var(--dsw-alias-bg-base); font-weight: 600; }
|
|
881
936
|
.gt-btn-primary:hover { border-color: var(--dsw-alias-brand-primary); }
|
|
@@ -892,22 +947,22 @@ window.__ModuleLoader__.load({
|
|
|
892
947
|
.gt-new { display: flex; flex-direction: column; gap: 10px; }
|
|
893
948
|
.gt-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
894
949
|
.gt-input, .gt-select, .gt-textarea { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary);
|
|
895
|
-
border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding:
|
|
950
|
+
border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 7px 10px; font: inherit; }
|
|
896
951
|
.gt-input:focus, .gt-select:focus, .gt-textarea:focus { outline: none; border-color: var(--dsw-alias-brand-primary); }
|
|
897
952
|
.gt-input { flex: 1; min-width: 120px; }
|
|
898
953
|
.gt-input-sm { flex: 0 1 auto; min-width: 96px; }
|
|
899
954
|
.gt-textarea { width: 100%; min-height: 54px; resize: vertical; font-family: inherit; }
|
|
900
955
|
.gt-select { padding: 6px 7px; }
|
|
901
|
-
.gt-search { display: flex; align-items: center; gap: 6px; flex: 1; min-width:
|
|
956
|
+
.gt-search { display: flex; align-items: center; gap: 6px; flex: 1 1 220px; min-width: 0;
|
|
902
957
|
background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px;
|
|
903
|
-
padding: 0 9px; color: var(--dsw-alias-label-secondary); }
|
|
958
|
+
min-height: 30px; box-sizing: border-box; padding: 0 9px; color: var(--dsw-alias-label-secondary); }
|
|
904
959
|
.gt-search input { flex: 1; background: transparent; border: none; color: var(--dsw-alias-label-primary);
|
|
905
|
-
font: inherit; padding:
|
|
960
|
+
font: inherit; font-size: 13px; padding: 0; min-width: 0; }
|
|
906
961
|
.gt-search input:focus { outline: none; }
|
|
907
962
|
.gt-search:focus-within { border-color: var(--dsw-alias-brand-primary); }
|
|
908
963
|
.gt-kbd { font-size: 10.5px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px;
|
|
909
964
|
padding: 0 4px; color: var(--dsw-alias-label-secondary); }
|
|
910
|
-
.gt-dir { display: inline-flex; align-items: center; gap: 5px; font-size:
|
|
965
|
+
.gt-dir { display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
|
|
911
966
|
color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l1);
|
|
912
967
|
border-radius: 8px; padding: 3px 6px 3px 8px; max-width: 560px; }
|
|
913
968
|
/* Only the path may shrink. Everything else in the pill used to shrink with it, so a
|
|
@@ -916,20 +971,30 @@ window.__ModuleLoader__.load({
|
|
|
916
971
|
.gt-dir > .gt-muted, .gt-dir > .gt-btn, .gt-dir > svg { white-space: nowrap; flex-shrink: 0; }
|
|
917
972
|
.gt-dir-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--dsw-alias-label-primary);
|
|
918
973
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 300px; }
|
|
919
|
-
.gt-toolbar { display:
|
|
974
|
+
.gt-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
|
|
975
|
+
gap: 8px 10px; }
|
|
976
|
+
.gt-toolbar-main { grid-column: 1 / -1; grid-row: 1; display: flex; align-items: center;
|
|
977
|
+
gap: 8px; min-width: 0; flex-wrap: nowrap; }
|
|
978
|
+
.gt-toolbar-main .gt-search { flex: 1 1 auto; }
|
|
979
|
+
.gt-toolbar > .gt-spacer { display: none; }
|
|
980
|
+
.gt-toolbar > .gt-context-summary { grid-row: 2; grid-column: 1; min-width: 0; }
|
|
981
|
+
.gt-toolbar > .gt-tabs { grid-row: 2; grid-column: 2; justify-self: end; }
|
|
982
|
+
.gt-toolbar > .gt-context-details { grid-column: 1 / -1; grid-row: 3; }
|
|
983
|
+
@media (max-width: 1250px) { .gt-toolbar-main { flex-wrap: wrap; } }
|
|
920
984
|
.gt-seg { display: inline-flex; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1);
|
|
921
985
|
border-radius: 9px; padding: 2px; gap: 2px; }
|
|
922
986
|
.gt-seg button { border: none; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit;
|
|
923
987
|
border-radius: 7px; padding: 3px 10px; cursor: pointer; }
|
|
924
|
-
.gt-seg button[data-on="1"] { background: var(--dsw-alias-
|
|
988
|
+
.gt-seg button[data-on="1"] { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
925
989
|
.gt-chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
926
990
|
/* One segmented style for every control that is "pick one of these": filters, grouping,
|
|
927
991
|
the two toggles. Chips stay for facts about a todo (tags, levels, notes). */
|
|
928
992
|
.gt-tabs { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
|
|
929
993
|
.gt-tab { border: none; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit;
|
|
930
|
-
font-size:
|
|
994
|
+
font-size: 12.5px; border-radius: 6px; padding: 0 10px; min-height: 24px; box-sizing: border-box;
|
|
995
|
+
cursor: pointer; white-space: nowrap; }
|
|
931
996
|
.gt-tab:hover { color: var(--dsw-alias-label-primary); }
|
|
932
|
-
.gt-tab[data-on="1"] { background: var(--dsw-alias-
|
|
997
|
+
.gt-tab[data-on="1"] { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
933
998
|
.gt-summary { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
934
999
|
.gt-browse { border: 1px solid var(--dsw-alias-border-l1); border-radius: 9px; padding: 8px 10px;
|
|
935
1000
|
display: flex; flex-direction: column; gap: 6px; max-height: 220px; overflow: auto; }
|
|
@@ -941,7 +1006,7 @@ window.__ModuleLoader__.load({
|
|
|
941
1006
|
.gt-browse-item:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-brand-primary); }
|
|
942
1007
|
.gt-divider { width: 1px; height: 16px; background: var(--dsw-alias-border-l1); }
|
|
943
1008
|
.gt-context { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px; }
|
|
944
|
-
.gt-chip { display: inline-flex; align-items: center; gap: 5px; font-size:
|
|
1009
|
+
.gt-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; line-height: 16px; padding: 2px 8px;
|
|
945
1010
|
border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); background: transparent;
|
|
946
1011
|
color: var(--dsw-alias-label-secondary); }
|
|
947
1012
|
button.gt-chip { cursor: pointer; font-family: inherit; }
|
|
@@ -951,27 +1016,31 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
951
1016
|
.gt-chip-warn { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
|
|
952
1017
|
.gt-chip-bad { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); }
|
|
953
1018
|
.gt-chip-ok { color: var(--dsw-alias-state-success-primary); }
|
|
954
|
-
.gt-groups { display: flex; flex-direction: column; gap:
|
|
1019
|
+
.gt-groups { display: flex; flex-direction: column; gap: 18px; overflow: auto; min-height: 0; flex: 1; }
|
|
955
1020
|
.gt-group { display: flex; flex-direction: column; gap: 6px; }
|
|
956
|
-
.gt-group-head { display: flex; align-items: center; gap:
|
|
957
|
-
font-size:
|
|
1021
|
+
.gt-group-head { display: flex; align-items: center; gap: 8px; color: var(--dsw-alias-label-secondary);
|
|
1022
|
+
font-size: 12px; line-height: 18px; padding: 0 2px; }
|
|
958
1023
|
.gt-group-head button { border: none; background: transparent; color: inherit; cursor: pointer;
|
|
959
1024
|
display: inline-flex; align-items: center; gap: 6px; font: inherit; padding: 2px 0; }
|
|
960
1025
|
.gt-group-head svg { transition: transform .12s ease; transform: rotate(90deg); }
|
|
961
1026
|
.gt-group[data-collapsed="1"] .gt-group-head svg { transform: rotate(0deg); }
|
|
962
1027
|
.gt-group-action { margin-left: 4px; border: 1px solid transparent; background: transparent; color: inherit;
|
|
963
|
-
border-radius: 999px; padding:
|
|
964
|
-
|
|
1028
|
+
border-radius: 999px; padding: 3px 8px; min-height: 24px; box-sizing: border-box; font: inherit;
|
|
1029
|
+
font-size: 0; letter-spacing: .04em; cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
|
|
1030
|
+
opacity: .72; transition: opacity .12s ease; }
|
|
1031
|
+
.gt-group:hover .gt-group-action, .gt-group-action:focus-visible { font-size: 11px; }
|
|
965
1032
|
.gt-group:hover .gt-group-action, .gt-group-action:focus-visible { opacity: 1; }
|
|
966
1033
|
.gt-group-action:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
|
|
967
1034
|
/* An exemption is a state, not an action: it stays visible and reads as "on" without a hover. */
|
|
968
1035
|
.gt-group-action[data-on="1"] { opacity: 1; color: var(--dsw-alias-label-primary);
|
|
969
1036
|
border-color: var(--dsw-alias-border-l2); }
|
|
970
|
-
.gt-group-count { color: var(--dsw-alias-label-secondary); }
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
.gt-
|
|
974
|
-
|
|
1037
|
+
.gt-group-count { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 400; }
|
|
1038
|
+
/* A group's level is redundant while it is expanded — the rows show it. */
|
|
1039
|
+
.gt-group:not([data-collapsed="1"]) .gt-group-head .gt-level { display: none; }
|
|
1040
|
+
.gt-group-label { color: var(--dsw-alias-label-primary); font-weight: 600; font-size: 12px;
|
|
1041
|
+
line-height: 18px; max-width: 480px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1042
|
+
.gt-level { display: inline-flex; align-items: center; gap: 3px; font-size: 11.5px; padding: 0;
|
|
1043
|
+
border: none; color: var(--dsw-alias-label-secondary); }
|
|
975
1044
|
.gt-level[data-level="urgent"] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); font-weight: 600; }
|
|
976
1045
|
.gt-level[data-level="high"] { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
|
|
977
1046
|
.gt-level[data-level="low"] { color: var(--dsw-alias-label-secondary); }
|
|
@@ -979,50 +1048,76 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
979
1048
|
display: inline-block; }
|
|
980
1049
|
.gt-here { font-size: 10.5px; padding: 0 5px; border-radius: 999px; border: 1px solid var(--dsw-alias-brand-primary);
|
|
981
1050
|
color: var(--dsw-alias-brand-primary); }
|
|
982
|
-
.gt-list { display: flex; flex-direction: column; gap:
|
|
983
|
-
.gt-item { position: relative; display: flex; flex-direction: column; gap:
|
|
1051
|
+
.gt-list { display: flex; flex-direction: column; gap: 8px; }
|
|
1052
|
+
.gt-item { position: relative; display: flex; flex-direction: column; gap: 8px;
|
|
984
1053
|
background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1);
|
|
985
|
-
border-radius:
|
|
1054
|
+
border-radius: 12px; padding: 12px 14px; }
|
|
986
1055
|
.gt-item:hover { border-color: var(--dsw-alias-border-l2); }
|
|
987
1056
|
.gt-item[data-status="doing"] { border-left: 2px solid var(--dsw-alias-brand-primary); }
|
|
988
1057
|
.gt-item[data-overdue="1"] { border-left: 2px solid var(--dsw-alias-state-error-primary); }
|
|
989
1058
|
.gt-item[data-status="done"], .gt-item[data-status="cancelled"] { opacity: .72; }
|
|
990
1059
|
.gt-item[data-open="1"] { border-color: var(--dsw-alias-brand-primary); }
|
|
991
1060
|
.gt-item-head { display: flex; gap: 10px; align-items: flex-start; }
|
|
992
|
-
.gt-check { flex: 0 0 auto; width:
|
|
1061
|
+
.gt-check { flex: 0 0 auto; width: 24px; height: 24px; margin-top: 0; border-radius: 999px; cursor: pointer;
|
|
993
1062
|
border: 1.4px solid var(--dsw-alias-border-l2); background: transparent; color: transparent;
|
|
994
1063
|
display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
995
1064
|
.gt-check:hover { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); }
|
|
996
1065
|
.gt-check[data-on="1"] { border-color: var(--dsw-alias-state-success-primary);
|
|
997
1066
|
background: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-bg-base); }
|
|
998
|
-
.gt-pick { flex: 0 0 auto; width:
|
|
1067
|
+
.gt-pick { flex: 0 0 auto; width: 22px; height: 22px; margin-top: 1px; border-radius: 6px; cursor: pointer;
|
|
999
1068
|
border: 1.4px solid var(--dsw-alias-border-l2); background: transparent; color: transparent;
|
|
1000
1069
|
display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
1001
1070
|
.gt-pick[data-on="1"] { border-color: var(--dsw-alias-brand-primary); background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-base); }
|
|
1002
1071
|
.gt-item[data-selected="1"] { border-color: var(--dsw-alias-brand-primary); }
|
|
1003
|
-
.gt-selectbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding:
|
|
1004
|
-
border: 1px solid var(--dsw-alias-brand-primary); border-radius:
|
|
1005
|
-
.gt-item-body { flex: 1; display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
|
1006
|
-
.gt-item-
|
|
1007
|
-
.gt-
|
|
1072
|
+
.gt-selectbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 9px 12px;
|
|
1073
|
+
border: 1px solid var(--dsw-alias-brand-primary); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); font-size: 13px; }
|
|
1074
|
+
.gt-item-body { flex: 1; display: flex; flex-direction: column; gap: 5px; min-width: 0; cursor: pointer; }
|
|
1075
|
+
.gt-item-body:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; border-radius: 6px; }
|
|
1076
|
+
.gt-item-title { font-weight: 600; font-size: 14px; word-break: break-word; line-height: 22px;
|
|
1077
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
1078
|
+
.gt-input-xs { width: 64px; flex: 0 0 auto; min-width: 0; padding: 0 8px; min-height: 28px;
|
|
1079
|
+
box-sizing: border-box; font-size: 12.5px; }
|
|
1008
1080
|
.gt-seq { font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary); }
|
|
1009
|
-
.gt-reminders { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px
|
|
1010
|
-
border: 1px solid var(--dsw-alias-
|
|
1011
|
-
background: var(--dsw-alias-bg-layer-1); font-size:
|
|
1012
|
-
.gt-reminders
|
|
1013
|
-
.gt-
|
|
1014
|
-
|
|
1015
|
-
|
|
1081
|
+
.gt-reminders { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px;
|
|
1082
|
+
min-height: 36px; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1);
|
|
1083
|
+
border-radius: 10px; background: var(--dsw-alias-bg-layer-1); font-size: 13px; }
|
|
1084
|
+
.gt-reminders { border-left: 2px solid var(--dsw-alias-state-warn-primary); }
|
|
1085
|
+
.gt-reminders-title { color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
1086
|
+
.gt-reminder-item { border: 1px solid transparent; background: transparent;
|
|
1087
|
+
color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; padding: 2px 6px;
|
|
1088
|
+
border-radius: 6px; cursor: pointer; max-width: 300px; overflow: hidden; text-overflow: ellipsis;
|
|
1016
1089
|
white-space: nowrap; }
|
|
1090
|
+
.gt-reminder-item:hover { background: var(--dsw-alias-button-ghost-active-fill); }
|
|
1091
|
+
.gt-settings { display: flex; flex-direction: column; gap: 12px; padding: 16px; width: 340px;
|
|
1092
|
+
box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px;
|
|
1093
|
+
background: var(--dsw-alias-bg-overlay); font-size: 13px; line-height: 20px; }
|
|
1094
|
+
.gt-settings-path { font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1095
|
+
.gt-settings .gt-input { flex: 0 0 auto; width: 100%; min-height: 32px; }
|
|
1096
|
+
.gt-settings .gt-check-line .gt-input { width: 64px; }
|
|
1097
|
+
.gt-settings-head { font-weight: 650; font-size: 14px; }
|
|
1098
|
+
.gt-check-line { display: flex; align-items: center; gap: 8px; justify-content: flex-start;
|
|
1099
|
+
color: var(--dsw-alias-label-primary); }
|
|
1100
|
+
.gt-settings-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
|
|
1101
|
+
.gt-context-summary { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
|
|
1102
|
+
.gt-context-summary .gt-chip { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1103
|
+
.gt-context-details { display: none; flex-direction: column; gap: 8px; padding: 10px 12px;
|
|
1104
|
+
border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px;
|
|
1105
|
+
background: var(--dsw-alias-bg-layer-1); }
|
|
1106
|
+
.gt-context-details[data-open="1"] { display: flex; }
|
|
1107
|
+
.gt-detail-head { display: flex; align-items: flex-start; gap: 8px; justify-content: space-between; }
|
|
1108
|
+
.gt-detail-title { font-size: 14px; line-height: 22px; font-weight: 600; min-width: 0;
|
|
1109
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1110
|
+
.gt-truncated { color: var(--dsw-alias-state-warn-primary); font-size: 12px; }
|
|
1111
|
+
.gt-bulk-tag { flex: 0 1 120px; min-width: 90px; }
|
|
1017
1112
|
.gt-inbox { margin-left: 6px; padding: 1px 6px; border-radius: 999px; font-size: 10.5px; font-weight: 600;
|
|
1018
1113
|
color: var(--dsw-alias-state-warn-primary); border: 1px solid var(--dsw-alias-state-warn-primary);
|
|
1019
1114
|
vertical-align: 1px; }
|
|
1020
1115
|
.gt-item[data-status="done"] .gt-item-title, .gt-item[data-status="cancelled"] .gt-item-title {
|
|
1021
1116
|
text-decoration: line-through; color: var(--dsw-alias-label-secondary); }
|
|
1022
|
-
.gt-meta { display: flex; gap:
|
|
1023
|
-
align-items: center; }
|
|
1117
|
+
.gt-meta { display: flex; gap: 2px 0; flex-wrap: wrap; color: var(--dsw-alias-label-secondary); font-size: 12px;
|
|
1118
|
+
line-height: 18px; align-items: center; }
|
|
1024
1119
|
.gt-meta > span { display: inline-flex; align-items: center; }
|
|
1025
|
-
.gt-meta
|
|
1120
|
+
.gt-meta > span + span::before { content: '·'; margin: 0 6px; opacity: .45; }
|
|
1026
1121
|
.gt-tag { color: var(--dsw-alias-brand-primary); }
|
|
1027
1122
|
/* The dispatch action stays put: hiding the only way to hand work to an agent
|
|
1028
1123
|
behind a hover makes a lone todo look like it can do nothing. */
|
|
@@ -1031,23 +1126,23 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1031
1126
|
.gt-attach .gt-chip { max-width: 280px; color: var(--dsw-alias-label-secondary); }
|
|
1032
1127
|
.gt-attach a.gt-chip:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
|
|
1033
1128
|
.gt-attach-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1034
|
-
.gt-menu { position: absolute; top: 40px; right: 10px; z-index: 3; min-width:
|
|
1035
|
-
flex-direction: column; padding:
|
|
1129
|
+
.gt-menu { position: absolute; top: 40px; right: 10px; z-index: 3; min-width: 184px; display: flex;
|
|
1130
|
+
flex-direction: column; padding: 5px; background: var(--dsw-alias-bg-overlay);
|
|
1036
1131
|
border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; }
|
|
1037
1132
|
.gt-menu button { display: flex; align-items: center; gap: 8px; text-align: left; border: none;
|
|
1038
|
-
background: transparent; color: var(--dsw-alias-label-primary); font: inherit; padding:
|
|
1133
|
+
background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; padding: 8px 10px;
|
|
1039
1134
|
border-radius: 7px; cursor: pointer; }
|
|
1040
|
-
.gt-menu button:hover { background: var(--dsw-alias-
|
|
1135
|
+
.gt-menu button:hover { background: var(--dsw-alias-button-ghost-active-fill); }
|
|
1041
1136
|
.gt-menu button[data-danger="1"] { color: var(--dsw-alias-state-error-primary); }
|
|
1042
|
-
.gt-detail { display: flex; flex-direction: column; gap:
|
|
1043
|
-
padding-top:
|
|
1137
|
+
.gt-detail { display: flex; flex-direction: column; gap: 12px; border-top: 1px solid var(--dsw-alias-border-l1);
|
|
1138
|
+
padding-top: 12px; margin-top: 2px; }
|
|
1044
1139
|
/* Wide screens: the open row's detail becomes a card on the right instead of pushing the list
|
|
1045
1140
|
down. Deliberately CSS-only over unchanged markup: one detail implementation, and it only
|
|
1046
1141
|
applies where there is room for a second column. */
|
|
1047
|
-
@media (min-width:
|
|
1142
|
+
@media (min-width: 1224px) {
|
|
1048
1143
|
/* Only the list gives way: the title row and the toolbar keep their full width, so the
|
|
1049
1144
|
controls never wrap or hide behind the card. */
|
|
1050
|
-
.gt-root[data-wide-detail="1"] .gt-groups { padding-right:
|
|
1145
|
+
.gt-root[data-wide-detail="1"] .gt-groups { padding-right: 360px; }
|
|
1051
1146
|
.gt-root[data-wide-detail="1"] .gt-inner { position: relative; }
|
|
1052
1147
|
.gt-root[data-wide-detail="1"] .gt-item[data-open="1"] .gt-detail {
|
|
1053
1148
|
/* Anchored to the panel's own column (.gt-inner), not to the viewport: the slot does not
|
|
@@ -1058,21 +1153,25 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1058
1153
|
a gap instead of overlapping by the padding width. */
|
|
1059
1154
|
box-sizing: border-box;
|
|
1060
1155
|
position: fixed; top: var(--gt-detail-top, 208px);
|
|
1061
|
-
right: var(--gt-detail-right, max(24px, calc((100vw -
|
|
1062
|
-
bottom: var(--gt-detail-bottom, 64px); width:
|
|
1156
|
+
right: var(--gt-detail-right, max(24px, calc((100vw - 1040px) / 2 + 24px)));
|
|
1157
|
+
bottom: var(--gt-detail-bottom, 64px); width: 340px;
|
|
1063
1158
|
overflow: auto; padding: 16px; margin: 0; border: 1px solid var(--dsw-alias-border-l2);
|
|
1064
1159
|
border-radius: 12px; background: var(--dsw-alias-bg-overlay); z-index: 30;
|
|
1065
1160
|
}
|
|
1066
1161
|
}
|
|
1067
1162
|
.gt-section { display: flex; flex-direction: column; gap: 5px; }
|
|
1068
1163
|
.gt-completion { white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
|
|
1069
|
-
.gt-section-title { color: var(--dsw-alias-label-secondary); font-size:
|
|
1070
|
-
letter-spacing: .06em; }
|
|
1164
|
+
.gt-section-title { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; font-weight: 600; }
|
|
1071
1165
|
.gt-pre { white-space: pre-wrap; word-break: break-word; margin: 0; line-height: 1.6; }
|
|
1072
1166
|
.gt-muted { color: var(--dsw-alias-label-secondary); }
|
|
1073
1167
|
.gt-links { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
|
1074
1168
|
.gt-link { display: flex; gap: 8px; align-items: center; padding: 4px 0; min-width: 0; }
|
|
1075
1169
|
.gt-link-main { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
|
|
1170
|
+
.gt-note-item { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
|
|
1171
|
+
.gt-note-item + .gt-note-item { border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
1172
|
+
.gt-note-meta { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--dsw-alias-label-secondary);
|
|
1173
|
+
white-space: nowrap; }
|
|
1174
|
+
.gt-note-text { font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-width: 68ch; }
|
|
1076
1175
|
.gt-link a { color: var(--dsw-alias-brand-primary); text-decoration: none; }
|
|
1077
1176
|
.gt-link a:hover { text-decoration: underline; }
|
|
1078
1177
|
.gt-link-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 320px; }
|
|
@@ -1434,9 +1533,19 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1434
1533
|
if (Number.isInteger(todo.seq) && todo.seq > 0) {
|
|
1435
1534
|
meta.push(h('span', { key: 'seq', className: 'gt-seq', title: t('meta.seqHint') }, `#${todo.seq}`))
|
|
1436
1535
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1536
|
+
if (todo.dueAt !== null) {
|
|
1537
|
+
meta.push(
|
|
1538
|
+
h(
|
|
1539
|
+
'span',
|
|
1540
|
+
{ key: 'due', className: overdue ? 'gt-chip-bad' : '', title: new Date(todo.dueAt).toLocaleString() },
|
|
1541
|
+
overdue
|
|
1542
|
+
? t('meta.overdue', { n: daysLate(todo.dueAt, now) })
|
|
1543
|
+
: sameLocalDay(Date.parse(todo.dueAt), now)
|
|
1544
|
+
? t('meta.today')
|
|
1545
|
+
: `${t('meta.due')} ${absoluteTime(Date.parse(todo.dueAt))}`,
|
|
1546
|
+
),
|
|
1547
|
+
)
|
|
1548
|
+
}
|
|
1440
1549
|
if (todo.priority !== 'normal') {
|
|
1441
1550
|
meta.push(
|
|
1442
1551
|
h(
|
|
@@ -1447,23 +1556,17 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1447
1556
|
),
|
|
1448
1557
|
)
|
|
1449
1558
|
}
|
|
1450
|
-
|
|
1559
|
+
meta.push(h('span', { key: 'added', title: new Date(todo.createdAt).toLocaleString() }, ageText(lang, todo.createdAt, now)))
|
|
1560
|
+
if (todo.tags.length > 0) {
|
|
1561
|
+
const shown = todo.tags.slice(0, 4)
|
|
1451
1562
|
meta.push(
|
|
1452
1563
|
h(
|
|
1453
1564
|
'span',
|
|
1454
|
-
{ key: '
|
|
1455
|
-
|
|
1456
|
-
? t('meta.overdue', { n: daysLate(todo.dueAt, now) })
|
|
1457
|
-
: sameLocalDay(Date.parse(todo.dueAt), now)
|
|
1458
|
-
? t('meta.today')
|
|
1459
|
-
: `${t('meta.due')} ${absoluteTime(Date.parse(todo.dueAt))}`,
|
|
1565
|
+
{ key: 'tags', className: 'gt-tag', title: todo.tags.map((tag) => `#${tag}`).join(' ') },
|
|
1566
|
+
shown.map((tag) => `#${tag}`).join(' ') + (todo.tags.length > shown.length ? ` +${todo.tags.length - shown.length}` : ''),
|
|
1460
1567
|
),
|
|
1461
1568
|
)
|
|
1462
1569
|
}
|
|
1463
|
-
if (todo.tags.length > 0) {
|
|
1464
|
-
meta.push(h('span', { key: 'tags', className: 'gt-tag' }, todo.tags.map((tag) => `#${tag}`).join(' ')))
|
|
1465
|
-
}
|
|
1466
|
-
meta.push(h('span', { key: 'by' }, t(`meta.by.${todo.createdBy === 'agent' ? 'agent' : 'human'}`)))
|
|
1467
1570
|
if (Array.isArray(todo.notes) && todo.notes.length > 0) {
|
|
1468
1571
|
const latest = todo.notes[todo.notes.length - 1]
|
|
1469
1572
|
meta.push(h('span', { key: 'notes', title: latest.text }, t('note.count', { n: todo.notes.length })))
|
|
@@ -1471,12 +1574,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1471
1574
|
if (todo.completion !== null && todo.completion !== undefined) {
|
|
1472
1575
|
meta.push(h('span', { key: 'completed', title: todo.completion.summary }, t('meta.completed', { when: absoluteTime(todo.completion.at) })))
|
|
1473
1576
|
}
|
|
1474
|
-
|
|
1475
|
-
meta.push(h('span', { key: 'origin', title: todo.originDir }, t('meta.origin', { dir: shortPath(todo.originDir) })))
|
|
1476
|
-
}
|
|
1577
|
+
meta.push(h('span', { key: 'by' }, t(`meta.by.${todo.createdBy === 'agent' ? 'agent' : 'human'}`)))
|
|
1477
1578
|
if (todo.sessionId !== '') {
|
|
1478
|
-
// A live conversation with no working directory cannot do the work. Say
|
|
1479
|
-
// so on the row instead of letting the next hand-off land there again.
|
|
1480
1579
|
const stranded = todo.sessionAlive === true && (todo.sessionDir ?? '') === ''
|
|
1481
1580
|
meta.push(
|
|
1482
1581
|
h(
|
|
@@ -1484,9 +1583,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1484
1583
|
{
|
|
1485
1584
|
key: 'session',
|
|
1486
1585
|
className: stranded ? 'gt-chip-warn' : todo.sessionAlive === true ? 'gt-chip-ok' : '',
|
|
1487
|
-
title: stranded
|
|
1488
|
-
? t('meta.session.noDirHint')
|
|
1489
|
-
: (todo.sessionDir ?? ''),
|
|
1586
|
+
title: stranded ? t('meta.session.noDirHint') : (todo.sessionDir ?? ''),
|
|
1490
1587
|
},
|
|
1491
1588
|
stranded ? t('meta.session.noDir') : todo.sessionAlive === true ? t('meta.session.live') : t('meta.session.bound'),
|
|
1492
1589
|
),
|
|
@@ -1551,7 +1648,21 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1551
1648
|
),
|
|
1552
1649
|
h(
|
|
1553
1650
|
'div',
|
|
1554
|
-
{
|
|
1651
|
+
{
|
|
1652
|
+
className: 'gt-item-body',
|
|
1653
|
+
role: 'button',
|
|
1654
|
+
tabIndex: 0,
|
|
1655
|
+
'aria-expanded': open,
|
|
1656
|
+
onClick: (event) => {
|
|
1657
|
+
if (event.target?.closest?.('a,button,input,textarea,select') !== null) return
|
|
1658
|
+
onToggleOpen(todo.id)
|
|
1659
|
+
},
|
|
1660
|
+
onKeyDown: (event) => {
|
|
1661
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
1662
|
+
event.preventDefault()
|
|
1663
|
+
onToggleOpen(todo.id)
|
|
1664
|
+
},
|
|
1665
|
+
},
|
|
1555
1666
|
h(
|
|
1556
1667
|
'div',
|
|
1557
1668
|
{ className: 'gt-item-title' },
|
|
@@ -1563,9 +1674,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1563
1674
|
h(
|
|
1564
1675
|
'div',
|
|
1565
1676
|
{ className: 'gt-meta' },
|
|
1566
|
-
meta
|
|
1567
|
-
index === 0 ? [node] : [h('span', { key: `sep${index}`, className: 'gt-meta-sep' }, '·'), node],
|
|
1568
|
-
),
|
|
1677
|
+
meta,
|
|
1569
1678
|
),
|
|
1570
1679
|
// What is attached is half the point of the list, so the first few are
|
|
1571
1680
|
// shown as chips instead of being reported as a number.
|
|
@@ -1664,7 +1773,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1664
1773
|
'button',
|
|
1665
1774
|
{
|
|
1666
1775
|
type: 'button',
|
|
1667
|
-
className: 'gt-btn gt-btn-
|
|
1776
|
+
className: 'gt-btn gt-btn-quiet',
|
|
1668
1777
|
disabled: busy,
|
|
1669
1778
|
title: dispatchLabel,
|
|
1670
1779
|
onClick: () => onAction('dispatch', todo),
|
|
@@ -1715,6 +1824,16 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1715
1824
|
? h(
|
|
1716
1825
|
'div',
|
|
1717
1826
|
{ className: 'gt-detail' },
|
|
1827
|
+
h(
|
|
1828
|
+
'div',
|
|
1829
|
+
{ className: 'gt-detail-head' },
|
|
1830
|
+
h('span', { className: 'gt-detail-title' }, `${Number.isInteger(todo.seq) && todo.seq > 0 ? `#${todo.seq} ` : ''}${todo.title}`),
|
|
1831
|
+
h(
|
|
1832
|
+
'button',
|
|
1833
|
+
{ type: 'button', className: 'gt-iconbtn', title: t('action.hide'), 'aria-label': t('action.hide'), onClick: () => onToggleOpen(todo.id) },
|
|
1834
|
+
h(Icon, { name: 'close', size: 13 }),
|
|
1835
|
+
),
|
|
1836
|
+
),
|
|
1718
1837
|
h(
|
|
1719
1838
|
'div',
|
|
1720
1839
|
{ className: 'gt-section' },
|
|
@@ -1799,14 +1918,15 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1799
1918
|
(todo.notes ?? []).map((entry, index) =>
|
|
1800
1919
|
h(
|
|
1801
1920
|
'li',
|
|
1802
|
-
{ className: 'gt-
|
|
1921
|
+
{ className: 'gt-note-item', key: `note:${index}:${entry.at}` },
|
|
1803
1922
|
h(
|
|
1804
|
-
'
|
|
1805
|
-
{ className: 'gt-
|
|
1806
|
-
h(Icon, { name: 'doc', size:
|
|
1807
|
-
h('span',
|
|
1808
|
-
h('span',
|
|
1923
|
+
'div',
|
|
1924
|
+
{ className: 'gt-note-meta' },
|
|
1925
|
+
h(Icon, { name: 'doc', size: 12 }),
|
|
1926
|
+
h('span', null, absoluteTime(entry.at)),
|
|
1927
|
+
h('span', null, entry.by === 'agent' ? t('note.by.agent') : t('note.by.human')),
|
|
1809
1928
|
),
|
|
1929
|
+
h('div', { className: 'gt-note-text' }, entry.text),
|
|
1810
1930
|
),
|
|
1811
1931
|
),
|
|
1812
1932
|
),
|
|
@@ -2042,6 +2162,22 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2042
2162
|
// A picked file is previewed first: a merge is not undoable, and "did I pick the right
|
|
2043
2163
|
// export" is exactly the kind of question a dry run answers before anything changes.
|
|
2044
2164
|
const [importAsk, setImportAsk] = useState(null)
|
|
2165
|
+
// One settings popover: reminders and the external door both need a place a person can
|
|
2166
|
+
// reach without curl.
|
|
2167
|
+
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
2168
|
+
const [settingsBox, setSettingsBox] = useState(null)
|
|
2169
|
+
const [contextOpen, setContextOpen] = useState(false)
|
|
2170
|
+
const [tokenDraft, setTokenDraft] = useState('')
|
|
2171
|
+
const [ingestDraft, setIngestDraft] = useState(false)
|
|
2172
|
+
const [belowDraft, setBelowDraft] = useState('0.8')
|
|
2173
|
+
const [remindersDraft, setRemindersDraft] = useState(true)
|
|
2174
|
+
const [leadDraft, setLeadDraft] = useState('0')
|
|
2175
|
+
const [notificationPerm, setNotificationPerm] = useState(
|
|
2176
|
+
typeof window !== 'undefined' && typeof window.Notification === 'function'
|
|
2177
|
+
? window.Notification.permission
|
|
2178
|
+
: 'unsupported',
|
|
2179
|
+
)
|
|
2180
|
+
const notifiedRef = useRef(new Set())
|
|
2045
2181
|
const [groupMode, setGroupMode] = useState(() => readStoredGroupMode())
|
|
2046
2182
|
// A notice may carry an action: after a hand-off the useful next step is
|
|
2047
2183
|
// to go and look at the conversation the work is actually in.
|
|
@@ -2270,6 +2406,26 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2270
2406
|
}
|
|
2271
2407
|
}, [lang])
|
|
2272
2408
|
|
|
2409
|
+
useEffect(() => {
|
|
2410
|
+
if (!settingsOpen || typeof window === 'undefined') {
|
|
2411
|
+
setSettingsBox(null)
|
|
2412
|
+
return undefined
|
|
2413
|
+
}
|
|
2414
|
+
const measure = () => {
|
|
2415
|
+
const element = innerRef.current
|
|
2416
|
+
if (element === null) return
|
|
2417
|
+
const rect = element.getBoundingClientRect()
|
|
2418
|
+
setSettingsBox({
|
|
2419
|
+
top: Math.round(rect.top + 210),
|
|
2420
|
+
right: Math.round(window.innerWidth - rect.right + 20),
|
|
2421
|
+
bottom: Math.round(window.innerHeight - rect.bottom + 20),
|
|
2422
|
+
})
|
|
2423
|
+
}
|
|
2424
|
+
measure()
|
|
2425
|
+
window.addEventListener('resize', measure)
|
|
2426
|
+
return () => window.removeEventListener('resize', measure)
|
|
2427
|
+
}, [settingsOpen, lang])
|
|
2428
|
+
|
|
2273
2429
|
useEffect(() => {
|
|
2274
2430
|
if (expandedId === '' || typeof window === 'undefined') {
|
|
2275
2431
|
setDetailAnchor(null)
|
|
@@ -2863,6 +3019,49 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2863
3019
|
return undefined
|
|
2864
3020
|
}
|
|
2865
3021
|
|
|
3022
|
+
/** Fill the popover from the host's state, so saving cannot send stale values. */
|
|
3023
|
+
const openSettings = () => {
|
|
3024
|
+
// One inspector at a time: the settings card and the detail card occupy the same side.
|
|
3025
|
+
setExpandedId('')
|
|
3026
|
+
const current = state.health?.settings ?? {}
|
|
3027
|
+
setIngestDraft(current.ingest?.enabled === true)
|
|
3028
|
+
setBelowDraft(String(current.ingest?.confirmBelow ?? 0.8))
|
|
3029
|
+
setRemindersDraft(current.remindersEnabled !== false)
|
|
3030
|
+
setLeadDraft(String(current.reminderLeadMinutes ?? 0))
|
|
3031
|
+
setTokenDraft('')
|
|
3032
|
+
setSettingsOpen(true)
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
const generateToken = () => {
|
|
3036
|
+
const bytes = new window.Uint8Array(24)
|
|
3037
|
+
window.crypto.getRandomValues(bytes)
|
|
3038
|
+
setTokenDraft(Array.from(bytes).map((byte) => byte.toString(16).padStart(2, '0')).join(''))
|
|
3039
|
+
setIngestDraft(true)
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
const saveSettings = async () => {
|
|
3043
|
+
try {
|
|
3044
|
+
await call('settings', {
|
|
3045
|
+
method: 'POST',
|
|
3046
|
+
body: {
|
|
3047
|
+
remindersEnabled: remindersDraft,
|
|
3048
|
+
reminderLeadMinutes: Number(leadDraft),
|
|
3049
|
+
ingestEnabled: ingestDraft,
|
|
3050
|
+
ingestConfirmBelow: Number(belowDraft),
|
|
3051
|
+
...(tokenDraft.trim() === '' ? {} : { ingestToken: tokenDraft.trim() }),
|
|
3052
|
+
},
|
|
3053
|
+
})
|
|
3054
|
+
setSettingsOpen(false)
|
|
3055
|
+
setTokenDraft('')
|
|
3056
|
+
await refreshHealth()
|
|
3057
|
+
setNotice({ text: t('settings.saved'), sessionId: '' })
|
|
3058
|
+
setTimeout(() => setNotice(null), 5000)
|
|
3059
|
+
} catch (failure) {
|
|
3060
|
+
reportFailure('settings', { message: failure?.message })
|
|
3061
|
+
setError(failure)
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
|
|
2866
3065
|
/** Acknowledge reminders so the host stops listing them. */
|
|
2867
3066
|
const ackReminders = async (ids) => {
|
|
2868
3067
|
try {
|
|
@@ -2901,6 +3100,30 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2901
3100
|
|
|
2902
3101
|
const counts = state.counts ?? { total: 0, open: 0, doing: 0, done: 0, cancelled: 0, archived: 0, overdue: 0, inbox: 0 }
|
|
2903
3102
|
const remindersDue = Array.isArray(state.health?.reminders?.due) ? state.health.reminders.due : []
|
|
3103
|
+
const dueKey = remindersDue.map((item) => `${item.id}:${item.dueAt}`).join('|')
|
|
3104
|
+
useEffect(() => {
|
|
3105
|
+
if (typeof window === 'undefined' || typeof window.Notification !== 'function') return
|
|
3106
|
+
for (const item of remindersDue) {
|
|
3107
|
+
const key = `${item.id}:${item.dueAt}`
|
|
3108
|
+
if (notifiedRef.current.has(key)) continue
|
|
3109
|
+
notifiedRef.current.add(key)
|
|
3110
|
+
if (window.Notification.permission !== 'granted') continue
|
|
3111
|
+
try {
|
|
3112
|
+
const note = new window.Notification(t('reminder.notifyTitle'), { body: item.title, tag: item.id })
|
|
3113
|
+
note.onclick = () => {
|
|
3114
|
+
try {
|
|
3115
|
+
window.focus()
|
|
3116
|
+
} catch {
|
|
3117
|
+
/* focus is a nicety */
|
|
3118
|
+
}
|
|
3119
|
+
setExpandedId(item.id)
|
|
3120
|
+
}
|
|
3121
|
+
} catch {
|
|
3122
|
+
/* a notification is a bonus, never a failure */
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3126
|
+
}, [dueKey])
|
|
2904
3127
|
const groups = useMemo(
|
|
2905
3128
|
() => groupTodos(state.todos, Date.now(), groupMode, sessionTitles, t),
|
|
2906
3129
|
[state.todos, groupMode, sessionTitles, lang],
|
|
@@ -2955,26 +3178,27 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2955
3178
|
'div',
|
|
2956
3179
|
{ className: 'gt-headline' },
|
|
2957
3180
|
h('div', { className: 'gt-title' }, t('panel.title')),
|
|
2958
|
-
h(
|
|
2959
|
-
'div',
|
|
2960
|
-
{ className: 'gt-sub' },
|
|
2961
|
-
t('panel.subtitle'),
|
|
2962
|
-
h('span', { className: 'gt-summary' }, ' · '),
|
|
2963
|
-
h('span', { className: 'gt-summary' }, t('summary.active', { n: counts.open + counts.doing })),
|
|
2964
|
-
counts.overdue > 0
|
|
2965
|
-
? h('span', { className: 'gt-summary' }, ` · ${t('summary.overdue', { n: counts.overdue })}`)
|
|
2966
|
-
: null,
|
|
2967
|
-
counts.done > 0
|
|
2968
|
-
? h('span', { className: 'gt-summary' }, ` · ${t('summary.done', { n: counts.done })}`)
|
|
2969
|
-
: null,
|
|
2970
|
-
),
|
|
3181
|
+
h('div', { className: 'gt-sub' }, t('panel.subtitle')),
|
|
2971
3182
|
),
|
|
2972
|
-
h('div', { className: 'gt-spacer' }),
|
|
2973
3183
|
h(
|
|
2974
|
-
'
|
|
2975
|
-
{
|
|
2976
|
-
|
|
2977
|
-
|
|
3184
|
+
'div',
|
|
3185
|
+
{ className: 'gt-head-actions' },
|
|
3186
|
+
h(
|
|
3187
|
+
'button',
|
|
3188
|
+
{
|
|
3189
|
+
type: 'button',
|
|
3190
|
+
className: 'gt-iconbtn',
|
|
3191
|
+
title: t('settings.title'),
|
|
3192
|
+
'aria-label': t('settings.title'),
|
|
3193
|
+
onClick: openSettings,
|
|
3194
|
+
},
|
|
3195
|
+
h(Icon, { name: 'gear' }),
|
|
3196
|
+
),
|
|
3197
|
+
h(
|
|
3198
|
+
'button',
|
|
3199
|
+
{
|
|
3200
|
+
type: 'button',
|
|
3201
|
+
className: 'gt-btn gt-btn-primary',
|
|
2978
3202
|
'aria-expanded': adding,
|
|
2979
3203
|
onClick: () => {
|
|
2980
3204
|
setAdding((value) => !value)
|
|
@@ -2984,32 +3208,17 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2984
3208
|
h(Icon, { name: 'plus', size: 13 }),
|
|
2985
3209
|
t('action.add'),
|
|
2986
3210
|
),
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
className: 'gt-btn gt-btn-quiet',
|
|
2999
|
-
title: t('action.exportHint'),
|
|
3000
|
-
onClick: () => void exportJson(),
|
|
3001
|
-
},
|
|
3002
|
-
t('action.export'),
|
|
3003
|
-
),
|
|
3004
|
-
h(
|
|
3005
|
-
'button',
|
|
3006
|
-
{
|
|
3007
|
-
type: 'button',
|
|
3008
|
-
className: 'gt-btn gt-btn-quiet',
|
|
3009
|
-
title: t('action.importHint'),
|
|
3010
|
-
onClick: () => importRef.current?.click?.(),
|
|
3011
|
-
},
|
|
3012
|
-
t('action.import'),
|
|
3211
|
+
// One overflow menu instead of four competing buttons: the primary action is
|
|
3212
|
+
// adding a todo; everything else is occasional.
|
|
3213
|
+
h(Menu, {
|
|
3214
|
+
label: t('action.overflow'),
|
|
3215
|
+
items: [
|
|
3216
|
+
{ key: 'refresh', label: t('action.refresh'), icon: 'refresh', onSelect: () => void reload() },
|
|
3217
|
+
{ key: 'export', label: t('action.export'), icon: 'doc', onSelect: () => void exportJson() },
|
|
3218
|
+
{ key: 'import', label: t('action.import'), icon: 'plus', onSelect: () => importRef.current?.click?.() },
|
|
3219
|
+
{ key: 'settings', label: t('settings.title'), icon: 'gear', onSelect: () => openSettings() },
|
|
3220
|
+
],
|
|
3221
|
+
}),
|
|
3013
3222
|
),
|
|
3014
3223
|
h('input', {
|
|
3015
3224
|
ref: importRef,
|
|
@@ -3020,12 +3229,105 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3020
3229
|
onChange: (event) => void importJson(event),
|
|
3021
3230
|
}),
|
|
3022
3231
|
),
|
|
3232
|
+
settingsOpen
|
|
3233
|
+
? h(
|
|
3234
|
+
'div',
|
|
3235
|
+
{
|
|
3236
|
+
className: 'gt-settings',
|
|
3237
|
+
role: 'dialog',
|
|
3238
|
+
'aria-label': t('settings.title'),
|
|
3239
|
+
style:
|
|
3240
|
+
settingsBox === null
|
|
3241
|
+
? undefined
|
|
3242
|
+
: {
|
|
3243
|
+
position: 'fixed',
|
|
3244
|
+
top: `${settingsBox.top}px`,
|
|
3245
|
+
right: `${settingsBox.right}px`,
|
|
3246
|
+
bottom: `${settingsBox.bottom}px`,
|
|
3247
|
+
maxHeight: 'none',
|
|
3248
|
+
overflow: 'auto',
|
|
3249
|
+
zIndex: 40,
|
|
3250
|
+
},
|
|
3251
|
+
},
|
|
3252
|
+
h('div', { className: 'gt-settings-head' }, t('settings.title')),
|
|
3253
|
+
h('div', { className: 'gt-section-title' }, t('settings.reminders')),
|
|
3254
|
+
h(
|
|
3255
|
+
'label',
|
|
3256
|
+
{ className: 'gt-check-line' },
|
|
3257
|
+
h('input', { type: 'checkbox', checked: remindersDraft, onChange: (event) => setRemindersDraft(event.target.checked) }),
|
|
3258
|
+
t('settings.remindersEnable'),
|
|
3259
|
+
),
|
|
3260
|
+
h(
|
|
3261
|
+
'label',
|
|
3262
|
+
{ className: 'gt-check-line' },
|
|
3263
|
+
t('settings.lead'),
|
|
3264
|
+
h('input', {
|
|
3265
|
+
type: 'number',
|
|
3266
|
+
min: 0,
|
|
3267
|
+
max: 1440,
|
|
3268
|
+
className: 'gt-input gt-input-xs',
|
|
3269
|
+
value: leadDraft,
|
|
3270
|
+
onChange: (event) => setLeadDraft(event.target.value),
|
|
3271
|
+
}),
|
|
3272
|
+
),
|
|
3273
|
+
h('div', { className: 'gt-section-title' }, t('settings.ingest')),
|
|
3274
|
+
h(
|
|
3275
|
+
'label',
|
|
3276
|
+
{ className: 'gt-check-line' },
|
|
3277
|
+
h('input', { type: 'checkbox', checked: ingestDraft, onChange: (event) => setIngestDraft(event.target.checked) }),
|
|
3278
|
+
t('settings.ingestEnable'),
|
|
3279
|
+
),
|
|
3280
|
+
h('input', {
|
|
3281
|
+
className: 'gt-input',
|
|
3282
|
+
type: 'password',
|
|
3283
|
+
value: tokenDraft,
|
|
3284
|
+
placeholder:
|
|
3285
|
+
state.health?.settings?.ingest?.tokenSet === true ? t('settings.tokenSet') : t('settings.token'),
|
|
3286
|
+
autoComplete: 'new-password',
|
|
3287
|
+
onChange: (event) => setTokenDraft(event.target.value),
|
|
3288
|
+
}),
|
|
3289
|
+
h(
|
|
3290
|
+
'div',
|
|
3291
|
+
{ className: 'gt-settings-row' },
|
|
3292
|
+
h('button', { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: generateToken }, t('settings.generate')),
|
|
3293
|
+
h('button', { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setSettingsOpen(false) }, t('settings.cancel')),
|
|
3294
|
+
h('button', { type: 'button', className: 'gt-btn gt-btn-primary', onClick: () => void saveSettings() }, t('settings.save')),
|
|
3295
|
+
),
|
|
3296
|
+
h(
|
|
3297
|
+
'label',
|
|
3298
|
+
{ className: 'gt-check-line' },
|
|
3299
|
+
t('settings.confirmBelow'),
|
|
3300
|
+
h('input', {
|
|
3301
|
+
type: 'number',
|
|
3302
|
+
min: 0,
|
|
3303
|
+
max: 1,
|
|
3304
|
+
step: 0.05,
|
|
3305
|
+
className: 'gt-input gt-input-xs',
|
|
3306
|
+
value: belowDraft,
|
|
3307
|
+
onChange: (event) => setBelowDraft(event.target.value),
|
|
3308
|
+
}),
|
|
3309
|
+
),
|
|
3310
|
+
h('div', { className: 'gt-section-title' }, t('settings.about')),
|
|
3311
|
+
h(
|
|
3312
|
+
'div',
|
|
3313
|
+
{ className: 'gt-muted gt-mono gt-settings-path', title: state.health?.store?.file ?? '' },
|
|
3314
|
+
state.health?.store?.file ?? '',
|
|
3315
|
+
),
|
|
3316
|
+
h('div', { className: 'gt-muted' }, `v${state.health?.version ?? ''} · ${UI_REV}`),
|
|
3317
|
+
)
|
|
3318
|
+
: null,
|
|
3319
|
+
|
|
3023
3320
|
error !== null
|
|
3024
3321
|
? h(
|
|
3025
3322
|
'div',
|
|
3026
3323
|
{ className: 'gt-banner', role: 'alert' },
|
|
3027
|
-
`${t('error.prefix')}: ${error.message}
|
|
3324
|
+
h('span', { className: 'gt-banner-text' }, `${t('error.prefix')}: ${error.message}`),
|
|
3028
3325
|
h('button', { type: 'button', className: 'gt-btn', onClick: () => void reload() }, t('action.retry')),
|
|
3326
|
+
h(
|
|
3327
|
+
'button',
|
|
3328
|
+
{ type: 'button', className: 'gt-iconbtn', title: t('action.hide'), 'aria-label': t('action.hide'), onClick: () => setError(null) },
|
|
3329
|
+
h(Icon, { name: 'close', size: 13 }),
|
|
3330
|
+
),
|
|
3029
3331
|
)
|
|
3030
3332
|
: null,
|
|
3031
3333
|
|
|
@@ -3115,6 +3417,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3115
3417
|
h(
|
|
3116
3418
|
'div',
|
|
3117
3419
|
{ className: 'gt-toolbar' },
|
|
3420
|
+
h('div', { className: 'gt-toolbar-main' },
|
|
3118
3421
|
h(
|
|
3119
3422
|
'span',
|
|
3120
3423
|
{ className: 'gt-tabs', role: 'group', 'aria-label': t('filter.label') },
|
|
@@ -3170,12 +3473,37 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3170
3473
|
),
|
|
3171
3474
|
),
|
|
3172
3475
|
),
|
|
3476
|
+
),
|
|
3173
3477
|
h('div', { className: 'gt-spacer' }),
|
|
3478
|
+
h(
|
|
3479
|
+
'div',
|
|
3480
|
+
{ className: 'gt-context-summary' },
|
|
3481
|
+
h(
|
|
3482
|
+
'button',
|
|
3483
|
+
{ type: 'button', className: 'gt-chip', title: effectiveDir === '' ? t('dir.none') : effectiveDir, onClick: () => setContextOpen((value) => !value) },
|
|
3484
|
+
h(Icon, { name: 'doc', size: 12 }),
|
|
3485
|
+
effectiveDir === '' ? t('dir.none') : effectiveDir,
|
|
3486
|
+
),
|
|
3487
|
+
h(
|
|
3488
|
+
'button',
|
|
3489
|
+
{ type: 'button', className: 'gt-chip', title: t('target.pick'), onClick: () => setContextOpen((value) => !value) },
|
|
3490
|
+
h(Icon, { name: 'play', size: 11 }),
|
|
3491
|
+
`${t('target.label')}: ${conversations.find((entry) => entry.id === targetValue)?.title ?? t('target.auto')}`,
|
|
3492
|
+
),
|
|
3493
|
+
h(
|
|
3494
|
+
'button',
|
|
3495
|
+
{ type: 'button', className: 'gt-btn gt-btn-quiet', 'aria-expanded': contextOpen, onClick: () => setContextOpen((value) => !value) },
|
|
3496
|
+
t('settings.context'),
|
|
3497
|
+
),
|
|
3498
|
+
),
|
|
3499
|
+
h(
|
|
3500
|
+
'div',
|
|
3501
|
+
{ className: 'gt-context-details', 'data-open': contextOpen ? '1' : '0' },
|
|
3174
3502
|
h(
|
|
3175
3503
|
'span',
|
|
3176
3504
|
{ className: 'gt-dir', title: effectiveDir === '' ? t('dir.none') : effectiveDir },
|
|
3177
3505
|
h(Icon, { name: 'doc', size: 12 }),
|
|
3178
|
-
h('span', { className: 'gt-muted' }, `${t('field.dir')}
|
|
3506
|
+
h('span', { className: 'gt-muted' }, `${t('field.dir')}: `),
|
|
3179
3507
|
h('span', { className: 'gt-dir-path' }, effectiveDir === '' ? t('dir.none') : effectiveDir),
|
|
3180
3508
|
h('span', { className: 'gt-muted' }, pickedDir === '' ? t('dir.fromSession') : t('dir.picked')),
|
|
3181
3509
|
h('button', { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => void chooseDir() }, t('action.pickDir')),
|
|
@@ -3280,7 +3608,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3280
3608
|
),
|
|
3281
3609
|
),
|
|
3282
3610
|
),
|
|
3283
|
-
h('span', { className: 'gt-muted' }, `${t('target.label')}
|
|
3611
|
+
h('span', { className: 'gt-muted' }, `${t('target.label')}: `),
|
|
3284
3612
|
h(
|
|
3285
3613
|
'select',
|
|
3286
3614
|
{
|
|
@@ -3310,6 +3638,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3310
3638
|
{ value: 'auto', label: t('target.auto') },
|
|
3311
3639
|
].map((option) => h('option', { key: option.value, value: option.value }, option.label)),
|
|
3312
3640
|
),
|
|
3641
|
+
),
|
|
3313
3642
|
h('span', { className: 'gt-spacer' }),
|
|
3314
3643
|
h('span', { className: 'gt-tabs', role: 'group', 'aria-label': t('autoCollect.label') }, togglesNodes),
|
|
3315
3644
|
),
|
|
@@ -3462,6 +3791,19 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3462
3791
|
`${item.seq > 0 ? `#${item.seq} ` : ''}${item.title}`,
|
|
3463
3792
|
),
|
|
3464
3793
|
),
|
|
3794
|
+
notificationPerm === 'default'
|
|
3795
|
+
? h(
|
|
3796
|
+
'button',
|
|
3797
|
+
{
|
|
3798
|
+
type: 'button',
|
|
3799
|
+
className: 'gt-btn gt-btn-quiet',
|
|
3800
|
+
onClick: () => {
|
|
3801
|
+
void window.Notification.requestPermission().then((permission) => setNotificationPerm(permission))
|
|
3802
|
+
},
|
|
3803
|
+
},
|
|
3804
|
+
t('reminder.enableNotifications'),
|
|
3805
|
+
)
|
|
3806
|
+
: null,
|
|
3465
3807
|
h(
|
|
3466
3808
|
'button',
|
|
3467
3809
|
{ type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => void ackReminders(remindersDue.map((item) => item.id)) },
|
|
@@ -3527,6 +3869,20 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3527
3869
|
'div',
|
|
3528
3870
|
{ className: 'gt-empty' },
|
|
3529
3871
|
h('div', { className: 'gt-empty-title' }, (state.counts?.total ?? 0) === 0 && query.trim() === '' ? t('empty.none') : t('empty.filtered')),
|
|
3872
|
+
(state.counts?.total ?? 0) === 0 && query.trim() === ''
|
|
3873
|
+
? h(
|
|
3874
|
+
'button',
|
|
3875
|
+
{
|
|
3876
|
+
type: 'button',
|
|
3877
|
+
className: 'gt-btn gt-btn-primary',
|
|
3878
|
+
onClick: () => {
|
|
3879
|
+
setAdding(true)
|
|
3880
|
+
setMore(false)
|
|
3881
|
+
},
|
|
3882
|
+
},
|
|
3883
|
+
t('empty.add'),
|
|
3884
|
+
)
|
|
3885
|
+
: null,
|
|
3530
3886
|
h('div', { className: 'gt-sub' }, t('hint.keys')),
|
|
3531
3887
|
)
|
|
3532
3888
|
: h(
|
|
@@ -3551,7 +3907,11 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3551
3907
|
},
|
|
3552
3908
|
h(Icon, { name: 'chevron', size: 12 }),
|
|
3553
3909
|
h('span', { className: 'gt-group-label' }, group.label),
|
|
3554
|
-
h(
|
|
3910
|
+
h(
|
|
3911
|
+
'span',
|
|
3912
|
+
{ className: 'gt-group-count' },
|
|
3913
|
+
group.todos.length === 1 ? t('group.item') : t('group.items', { n: group.todos.length }),
|
|
3914
|
+
),
|
|
3555
3915
|
group.level === 'normal'
|
|
3556
3916
|
? null
|
|
3557
3917
|
: h(
|
|
@@ -3617,7 +3977,10 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3617
3977
|
open: expandedId === todo.id,
|
|
3618
3978
|
dispatchLabel,
|
|
3619
3979
|
onAction,
|
|
3620
|
-
onToggleOpen: (id) =>
|
|
3980
|
+
onToggleOpen: (id) => {
|
|
3981
|
+
setSettingsOpen(false)
|
|
3982
|
+
setExpandedId((current) => (current === id ? '' : id))
|
|
3983
|
+
},
|
|
3621
3984
|
}),
|
|
3622
3985
|
),
|
|
3623
3986
|
),
|
|
@@ -3633,15 +3996,6 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3633
3996
|
)
|
|
3634
3997
|
: null,
|
|
3635
3998
|
|
|
3636
|
-
h(
|
|
3637
|
-
'div',
|
|
3638
|
-
{ className: 'gt-foot' },
|
|
3639
|
-
state.health !== null ? h('span', null, t('store.at', { file: state.health.store?.file ?? '' })) : null,
|
|
3640
|
-
state.health !== null
|
|
3641
|
-
? h('span', { className: 'gt-mono' }, `v${state.health.version ?? ''}·${UI_REV}`)
|
|
3642
|
-
: null,
|
|
3643
|
-
h('span', null, t('hint.keys')),
|
|
3644
|
-
),
|
|
3645
3999
|
),
|
|
3646
4000
|
notice !== null && notice !== undefined
|
|
3647
4001
|
? h(
|
package/lib/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import { createStore, resolveStorePath } from './store.js'
|
|
|
36
36
|
import { createTools } from './tools.js'
|
|
37
37
|
|
|
38
38
|
/** Must equal `package.json`'s version; `npm run check` enforces that. */
|
|
39
|
-
export const VERSION = '0.
|
|
39
|
+
export const VERSION = '0.6.0'
|
|
40
40
|
|
|
41
41
|
/** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
|
|
42
42
|
export const name = 'dsh-global-todo'
|
|
@@ -418,8 +418,17 @@ export function apply(ctx, config) {
|
|
|
418
418
|
ctx.effect(() => {
|
|
419
419
|
const tick = async () => {
|
|
420
420
|
try {
|
|
421
|
+
const current = settings.get()
|
|
422
|
+
if (current.remindersEnabled === false) {
|
|
423
|
+
reminderState.due = []
|
|
424
|
+
return
|
|
425
|
+
}
|
|
421
426
|
const { todos } = await service.list({ status: 'active' })
|
|
422
|
-
const due = dueReminders(todos, {
|
|
427
|
+
const due = dueReminders(todos, {
|
|
428
|
+
now: Date.now(),
|
|
429
|
+
reminded: reminderState.reminded,
|
|
430
|
+
leadMs: (current.reminderLeadMinutes ?? 0) * 60_000,
|
|
431
|
+
})
|
|
423
432
|
if (due.length === 0) return
|
|
424
433
|
for (const item of due) reminderState.reminded[item.id] = item.dueAt
|
|
425
434
|
const known = new Set(reminderState.due.map((item) => item.id))
|
package/lib/reminders.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export function dueReminders(todos, options = {}) {
|
|
19
19
|
const now = Number.isFinite(options.now) ? options.now : Date.now()
|
|
20
|
+
// A lead time turns "at the deadline" into "a little before it": the same rule, shifted.
|
|
21
|
+
const lead = Number.isFinite(options.leadMs) && options.leadMs > 0 ? options.leadMs : 0
|
|
20
22
|
const reminded = options.reminded !== null && typeof options.reminded === 'object' ? options.reminded : {}
|
|
21
23
|
const due = []
|
|
22
24
|
for (const todo of Array.isArray(todos) ? todos : []) {
|
|
@@ -24,7 +26,7 @@ export function dueReminders(todos, options = {}) {
|
|
|
24
26
|
if (todo.status !== 'open' && todo.status !== 'doing') continue
|
|
25
27
|
if (todo.dueAt === null || todo.dueAt === undefined || typeof todo.dueAt !== 'string') continue
|
|
26
28
|
const at = Date.parse(todo.dueAt)
|
|
27
|
-
if (!Number.isFinite(at) || at > now) continue
|
|
29
|
+
if (!Number.isFinite(at) || at - lead > now) continue
|
|
28
30
|
if (reminded[todo.id] === todo.dueAt) continue
|
|
29
31
|
due.push({
|
|
30
32
|
id: todo.id,
|
package/lib/routes.js
CHANGED
|
@@ -417,6 +417,8 @@ export function createRoutes(deps) {
|
|
|
417
417
|
loadError: settingsInfo.error,
|
|
418
418
|
// The token itself never leaves the settings file; the panel only needs to know
|
|
419
419
|
// whether one is set before it can usefully show the ingest section.
|
|
420
|
+
remindersEnabled: deps.settings?.get?.().remindersEnabled !== false,
|
|
421
|
+
reminderLeadMinutes: deps.settings?.get?.().reminderLeadMinutes ?? 0,
|
|
420
422
|
ingest: {
|
|
421
423
|
enabled: deps.settings?.get?.().ingestEnabled === true,
|
|
422
424
|
sources: deps.settings?.get?.().ingestSources ?? [],
|
package/lib/settings.js
CHANGED
|
@@ -66,6 +66,10 @@ export const SETTINGS_DEFAULTS = Object.freeze({
|
|
|
66
66
|
ingestSources: [...INGEST_SOURCES],
|
|
67
67
|
/** Items below this confidence wait in the inbox instead of joining the list. */
|
|
68
68
|
ingestConfirmBelow: 0.8,
|
|
69
|
+
/** Announce deadlines in the panel. On by default; a deadline nobody hears about is just data. */
|
|
70
|
+
remindersEnabled: true,
|
|
71
|
+
/** Announce this many minutes before the deadline (0 = at the deadline). */
|
|
72
|
+
reminderLeadMinutes: 0,
|
|
69
73
|
})
|
|
70
74
|
|
|
71
75
|
/**
|
|
@@ -105,6 +109,7 @@ export function normalizeSettings(raw) {
|
|
|
105
109
|
sources.push(kind)
|
|
106
110
|
}
|
|
107
111
|
const below = Number(source.ingestConfirmBelow)
|
|
112
|
+
const lead = Number(source.reminderLeadMinutes)
|
|
108
113
|
return {
|
|
109
114
|
autoCollect: source.autoCollect === true,
|
|
110
115
|
cooldownMinutes: Number.isInteger(minutes) ? Math.min(Math.max(minutes, 5), 24 * 60) : SETTINGS_DEFAULTS.cooldownMinutes,
|
|
@@ -117,6 +122,8 @@ export function normalizeSettings(raw) {
|
|
|
117
122
|
ingestToken: typeof source.ingestToken === 'string' ? source.ingestToken.trim().slice(0, 512) : '',
|
|
118
123
|
ingestSources: sources.length === 0 ? [...INGEST_SOURCES] : sources,
|
|
119
124
|
ingestConfirmBelow: Number.isFinite(below) ? Math.min(Math.max(below, 0), 1) : SETTINGS_DEFAULTS.ingestConfirmBelow,
|
|
125
|
+
remindersEnabled: source.remindersEnabled !== false,
|
|
126
|
+
reminderLeadMinutes: Number.isFinite(lead) ? Math.min(Math.max(lead, 0), 24 * 60) : SETTINGS_DEFAULTS.reminderLeadMinutes,
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-global-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"icon": "./docs/icon.svg",
|
|
5
5
|
"description": "Global, cross-session todos for DeepSeek Harness: one list owned by the human and the agent together, with creation time, attached documents and links, and one-click hand-off to an agent session. 全局待办:跨会话一份清单,记录加入时间,可挂文档与地址,可一键交给 Agent 继续干。",
|
|
6
6
|
"type": "module",
|