dsh-worktodo 0.1.0 → 0.2.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/lib/client.js CHANGED
@@ -36,6 +36,139 @@ __export(index_exports, {
36
36
  });
37
37
  module.exports = __toCommonJS(index_exports);
38
38
  var import_react = __toESM(require("react"), 1);
39
+
40
+ // src/parse-due.mjs
41
+ var DAY_MS = 24 * 60 * 60 * 1e3;
42
+ function dayKey(stamp) {
43
+ const d = new Date(stamp);
44
+ const pad = (n) => String(n).padStart(2, "0");
45
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
46
+ }
47
+ function weekStartKey(stamp) {
48
+ const d = new Date(stamp);
49
+ const day = (d.getDay() + 6) % 7;
50
+ const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate() - day);
51
+ return dayKey(monday.getTime());
52
+ }
53
+ var CN_NUM = { \u96F6: 0, \u4E00: 1, \u4E24: 2, \u4E8C: 2, \u4E09: 3, \u56DB: 4, \u4E94: 5, \u516D: 6, \u4E03: 7, \u516B: 8, \u4E5D: 9, \u5341: 10 };
54
+ var WEEKDAY = { \u4E00: 1, \u4E8C: 2, \u4E09: 3, \u56DB: 4, \u4E94: 5, \u516D: 6, \u65E5: 7, \u5929: 7 };
55
+ var PERIOD_HOUR = { \u4ECA\u65E9: 9, \u65E9\u4E0A: 9, \u65E9\u6668: 9, \u4E0A\u5348: 9, \u4E2D\u5348: 12, \u4E0B\u5348: 15, \u508D\u665A: 18, \u4ECA\u665A: 20, \u660E\u665A: 20, \u665A\u4E0A: 20, \u665A: 20, \u591C\u91CC: 21 };
56
+ var toCnNumber = (raw) => {
57
+ if (/^\d+$/.test(raw)) return Number(raw);
58
+ if (raw === "\u5341") return 10;
59
+ if (raw.startsWith("\u5341")) return 10 + (CN_NUM[raw[1]] ?? 0);
60
+ if (raw.includes("\u5341")) {
61
+ const [a, b] = raw.split("\u5341");
62
+ return (CN_NUM[a] ?? 1) * 10 + (CN_NUM[b] ?? 0);
63
+ }
64
+ return CN_NUM[raw] ?? null;
65
+ };
66
+ function parseNaturalDue(text, now = Date.now()) {
67
+ const source = typeof text === "string" ? text : "";
68
+ const spans = [];
69
+ let due = null;
70
+ let dueTime = null;
71
+ let hourFromPeriod = null;
72
+ const today = dayKey(now);
73
+ const todayStart = (/* @__PURE__ */ new Date(`${today}T00:00:00`)).getTime();
74
+ let m = /(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})日?/.exec(source);
75
+ if (m) {
76
+ due = dayKey(new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime());
77
+ spans.push({ start: m.index, end: m.index + m[0].length, kind: "date" });
78
+ } else {
79
+ m = /(\d{1,2})月(\d{1,2})日?/.exec(source);
80
+ if (m) {
81
+ let year = new Date(now).getFullYear();
82
+ let candidate = dayKey(new Date(year, Number(m[1]) - 1, Number(m[2])).getTime());
83
+ if (candidate < today) candidate = dayKey(new Date(year + 1, Number(m[1]) - 1, Number(m[2])).getTime());
84
+ due = candidate;
85
+ spans.push({ start: m.index, end: m.index + m[0].length, kind: "date" });
86
+ }
87
+ }
88
+ const weekRe = /(下下|下个|下|本|这)?(?:周|星期|礼拜)([一二三四五六日天1-7])/g;
89
+ for (let w = weekRe.exec(source); w; w = weekRe.exec(source)) {
90
+ const offsetWeeks = w[1] === "\u4E0B" || w[1] === "\u4E0B\u4E2A" ? 1 : w[1] === "\u4E0B\u4E0B" ? 2 : 0;
91
+ const target = WEEKDAY[w[2]] ?? Number(w[2]);
92
+ const thisWeekStart = (/* @__PURE__ */ new Date(`${weekStartKey(now)}T00:00:00`)).getTime();
93
+ let stamp = thisWeekStart + (target - 1) * DAY_MS + offsetWeeks * 7 * DAY_MS;
94
+ if (offsetWeeks === 0 && stamp < todayStart) stamp += 7 * DAY_MS;
95
+ const candidate = dayKey(stamp);
96
+ if (due === null || spans.some((s) => s.kind === "date" && s.weekday)) {
97
+ spans.filter((s) => s.weekday).forEach((s) => {
98
+ s.dropped = true;
99
+ });
100
+ }
101
+ due = candidate;
102
+ spans.push({ start: w.index, end: w.index + w[0].length, kind: "date", weekday: true });
103
+ }
104
+ if (due === null) {
105
+ const relWords = { \u4ECA\u5929: 0, \u4ECA\u65E5: 0, \u660E\u5929: 1, \u660E\u65E5: 1, \u540E\u5929: 2, \u5927\u540E\u5929: 3, \u6628\u5929: -1, \u6628: -1 };
106
+ for (const [word, offset] of Object.entries(relWords)) {
107
+ const at = source.indexOf(word);
108
+ if (at !== -1) {
109
+ due = dayKey(now + offset * DAY_MS);
110
+ spans.push({ start: at, end: at + word.length, kind: "date" });
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ if (due === null) {
116
+ const plus = /\+(\d{1,3})\s*d(?:ay)?s?/.exec(source);
117
+ const cn = /([0-9零一两二三四五六七八九十]+)\s*天\s*(?:后|以后|之后)/.exec(source);
118
+ if (plus) {
119
+ due = dayKey(now + Number(plus[1]) * DAY_MS);
120
+ spans.push({ start: plus.index, end: plus.index + plus[0].length, kind: "date" });
121
+ } else if (cn) {
122
+ const n = toCnNumber(cn[1]);
123
+ if (n !== null) {
124
+ due = dayKey(now + n * DAY_MS);
125
+ spans.push({ start: cn.index, end: cn.index + cn[0].length, kind: "date" });
126
+ }
127
+ }
128
+ }
129
+ const periodRe = /(今晚|明晚|明早|早上|早晨|上午|中午|下午|傍晚|晚上|夜里)/;
130
+ const period = periodRe.exec(source);
131
+ if (period) {
132
+ hourFromPeriod = PERIOD_HOUR[period[1]] ?? null;
133
+ if (due === null) {
134
+ if (period[1] === "\u4ECA\u665A" || period[1] === "\u660E\u665A") due = dayKey(now + (period[1] === "\u660E\u665A" ? 1 : 0) * DAY_MS);
135
+ if (period[1] === "\u660E\u65E9") due = dayKey(now + DAY_MS);
136
+ }
137
+ spans.push({ start: period.index, end: period.index + period[1].length, kind: "time" });
138
+ }
139
+ const hm = /(\d{1,2})[::](\d{2})/.exec(source);
140
+ const cnHour = /([0-9零一两二三四五六七八九十]+)\s*[点时](半|[0-9零一两二三四五六七八九十]{1,3}分)?/.exec(source);
141
+ if (hm) {
142
+ const h = Number(hm[1]);
143
+ if (h <= 23 && Number(hm[2]) <= 59) dueTime = `${String(h).padStart(2, "0")}:${hm[2]}`;
144
+ spans.push({ start: hm.index, end: hm.index + hm[0].length, kind: "time" });
145
+ } else if (cnHour) {
146
+ let h = toCnNumber(cnHour[1]);
147
+ if (h !== null && h <= 23) {
148
+ if (h <= 12 && hourFromPeriod !== null && hourFromPeriod >= 12) h = h === 12 ? 12 : h + 12;
149
+ const minute = cnHour[2] === "\u534A" ? 30 : cnHour[2] ? toCnNumber(cnHour[2].replace("\u5206", "")) ?? 0 : 0;
150
+ if (minute <= 59) dueTime = `${String(h).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
151
+ spans.push({ start: cnHour.index, end: cnHour.index + cnHour[0].length, kind: "time" });
152
+ }
153
+ } else if (hourFromPeriod !== null) {
154
+ dueTime = `${String(hourFromPeriod).padStart(2, "0")}:00`;
155
+ }
156
+ if (dueTime && !due) due = today;
157
+ const kept = spans.filter((s) => !s.dropped).sort((a, b) => a.start - b.start);
158
+ let title = source;
159
+ for (const span of [...kept].reverse()) {
160
+ title = title.slice(0, span.start) + " " + title.slice(span.end);
161
+ }
162
+ title = title.replace(/\s+/g, " ").trim();
163
+ return {
164
+ title: title || source.trim(),
165
+ due,
166
+ dueTime,
167
+ matched: kept.map((s) => source.slice(s.start, s.end))
168
+ };
169
+ }
170
+
171
+ // src/client/index.js
39
172
  var PKG_NAME = true ? "dsh-worktodo" : "dsh-worktodo";
40
173
  var DERIVED_NAME = PKG_NAME.replace(/^dsh-/, "");
41
174
  var SHORT_NAME = DERIVED_NAME === "worktodo" ? DERIVED_NAME : "worktodo";
@@ -71,6 +204,26 @@ var zh = {
71
204
  "groupWeek": "\u672C\u5468",
72
205
  "groupLater": "\u4EE5\u540E",
73
206
  "groupCompleted": "\u5DF2\u5B8C\u6210",
207
+ "viewDue": "\u6309\u622A\u6B62",
208
+ "viewProject": "\u6309\u9879\u76EE",
209
+ "noProject": "\u672A\u5F52\u7C7B",
210
+ "sortLabel": "\u6392\u5E8F",
211
+ "sortManual": "\u624B\u52A8",
212
+ "sortPriority": "\u4F18\u5148\u7EA7",
213
+ "sortCreated": "\u521B\u5EFA\u65F6\u95F4",
214
+ "dragHint": "\u62D6\u52A8\u8C03\u6574\u987A\u5E8F\uFF08\u4F1A\u81EA\u52A8\u5207\u5230\u624B\u52A8\u6392\u5E8F\uFF09",
215
+ "versionLabel": "\u7248\u672C",
216
+ "versionHint": "\u5F53\u524D\u5B89\u88C5\u7684 dsh-worktodo \u7248\u672C",
217
+ "updateAvailable": "\u6709\u65B0\u7248\u672C",
218
+ "updateHint": "\u66F4\u65B0\u547D\u4EE4\uFF08\u91CD\u542F DSH \u751F\u6548\uFF09",
219
+ "addOpen": "\u65B0\u589E\u5F85\u529E",
220
+ "addCancel": "\u53D6\u6D88",
221
+ "prioNone": "\u65E0",
222
+ "dateUnset": "\u65E5\u671F \xB7 \u672A\u8BBE",
223
+ "timeUnset": "\u65F6\u95F4 \xB7 \u672A\u8BBE",
224
+ "todayWord": "\u4ECA\u5929",
225
+ "fromParsed": "\u8BC6\u522B",
226
+ "dueTimeLabel": "\u65F6\u95F4",
74
227
  "expand": "\u5C55\u5F00",
75
228
  "collapse": "\u6536\u8D77",
76
229
  "empty": "\u8FD8\u6CA1\u6709\u5F85\u529E",
@@ -129,6 +282,26 @@ var en = {
129
282
  "groupWeek": "This week",
130
283
  "groupLater": "Later",
131
284
  "groupCompleted": "Completed",
285
+ "viewDue": "By due",
286
+ "viewProject": "By project",
287
+ "noProject": "Uncategorized",
288
+ "sortLabel": "Sort",
289
+ "sortManual": "Manual",
290
+ "sortPriority": "Priority",
291
+ "sortCreated": "Created",
292
+ "dragHint": "Drag to reorder (switches to manual sort)",
293
+ "versionLabel": "Version",
294
+ "versionHint": "Installed dsh-worktodo version",
295
+ "updateAvailable": "Update available",
296
+ "updateHint": "Update command (restart DSH to apply)",
297
+ "addOpen": "New todo",
298
+ "addCancel": "Cancel",
299
+ "prioNone": "None",
300
+ "dateUnset": "Date \xB7 unset",
301
+ "timeUnset": "Time \xB7 unset",
302
+ "todayWord": "Today",
303
+ "fromParsed": "parsed",
304
+ "dueTimeLabel": "Time",
132
305
  "expand": "Expand",
133
306
  "collapse": "Collapse",
134
307
  "empty": "No to-dos yet",
@@ -159,6 +332,11 @@ var en = {
159
332
  "titleEmpty": "Type something first",
160
333
  "untitled": "(untitled)"
161
334
  };
335
+ function dayKey2(stamp) {
336
+ const d = new Date(stamp);
337
+ const pad = (n) => String(n).padStart(2, "0");
338
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
339
+ }
162
340
  var C = {
163
341
  label: "var(--dsw-alias-label-primary)",
164
342
  label2: "var(--dsw-alias-label-secondary)",
@@ -226,6 +404,32 @@ var STYLE_TEXT = `
226
404
  flex-direction: column; align-items: stretch; gap: 6px;
227
405
  border: 1px solid ${C.border1}; background: ${C.layer1};
228
406
  }
407
+ .wt-seg {
408
+ display: inline-flex; align-items: center; border-radius: 8px; overflow: hidden;
409
+ border: 1px solid ${C.border}; background: ${C.bg2};
410
+ }
411
+ .wt-seg-btn {
412
+ appearance: none; border: 0; background: transparent; color: ${C.label2};
413
+ font: inherit; font-size: 11.5px; line-height: 1; padding: 5px 9px; cursor: pointer;
414
+ }
415
+ .wt-seg-btn.is-on { background: ${C.hover}; color: ${C.label}; font-weight: 600; }
416
+ .wt-seg-btn:hover { color: ${C.label}; }
417
+ .wt-row[draggable="true"] { cursor: grab; }
418
+ .wt-row[draggable="true"]:active { cursor: grabbing; }
419
+ .wt-plus {
420
+ width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
421
+ padding: 12px 14px; margin-bottom: 10px; cursor: pointer; font: inherit; font-size: 13.5px; font-weight: 600;
422
+ color: ${C.label2}; background: ${C.layer1};
423
+ border: 1px dashed ${C.border}; border-radius: 12px;
424
+ }
425
+ .wt-plus:hover { color: ${C.label}; border-color: ${C.brand}; background: ${tint(C.brand, 8)}; }
426
+ .wt-plus-mark { font-size: 24px; line-height: 1; font-weight: 400; color: ${C.brand}; }
427
+ .wt-flag-btn {
428
+ appearance: none; display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
429
+ font: inherit; font-size: 11.5px; padding: 5px 9px; border-radius: 9px;
430
+ background: transparent; border: 1px solid ${C.border}; color: ${C.label2};
431
+ }
432
+ .wt-flag-btn.is-on { border-color: currentColor; background: color-mix(in srgb, currentColor 12%, transparent); color: inherit; }
229
433
  .wt-group-head {
230
434
  display: flex; align-items: center; gap: 7px; padding: 3px 8px; border-radius: 8px;
231
435
  cursor: pointer; user-select: none; color: ${C.label2};
@@ -362,12 +566,13 @@ async function postJson(path, body) {
362
566
  return parsed.data ?? {};
363
567
  }
364
568
  var lastList = null;
365
- function useList() {
569
+ var lastSort = "manual";
570
+ function useList(sort) {
366
571
  const [state, setState] = import_react.default.useState(() => lastList ? { status: "ready", data: lastList, error: null } : { status: "loading", data: null, error: null });
367
572
  const [tick, setTick] = import_react.default.useState(0);
368
573
  import_react.default.useEffect(() => {
369
574
  let alive = true;
370
- fetch(`${ROUTE_LIST}?filter=all`, { cache: "no-store", credentials: "same-origin" }).then(async (response) => {
575
+ fetch(`${ROUTE_LIST}?filter=all&sort=${encodeURIComponent(sort || "manual")}`, { cache: "no-store", credentials: "same-origin" }).then(async (response) => {
371
576
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
372
577
  const body = await response.json().catch(() => null);
373
578
  if (!body?.ok) throw new Error(body?.error ?? body?.reason ?? "\u672A\u77E5\u9519\u8BEF");
@@ -385,79 +590,257 @@ function useList() {
385
590
  return () => {
386
591
  alive = false;
387
592
  };
388
- }, [tick]);
593
+ }, [tick, sort]);
389
594
  const refresh = import_react.default.useCallback(() => setTick((n) => n + 1), []);
390
595
  return { ...state, refresh };
391
596
  }
597
+ function IconFlag({ filled, dashed }) {
598
+ return import_react.default.createElement("svg", { width: 13, height: 13, viewBox: "0 0 16 16", "aria-hidden": "true" }, [
599
+ import_react.default.createElement("path", {
600
+ key: "pole",
601
+ d: "M3.6 1.6v13",
602
+ stroke: "currentColor",
603
+ strokeWidth: 1.6,
604
+ strokeLinecap: "round",
605
+ fill: "none",
606
+ strokeDasharray: dashed ? "1.5 1.5" : void 0
607
+ }),
608
+ import_react.default.createElement("path", {
609
+ key: "cloth",
610
+ d: "M5.2 2.4h7.4L11 5.2l1.6 2.8H5.2z",
611
+ fill: filled ? "currentColor" : "none",
612
+ stroke: "currentColor",
613
+ strokeWidth: 1.3,
614
+ strokeLinejoin: "round",
615
+ strokeDasharray: dashed ? "1.5 1.5" : void 0
616
+ })
617
+ ]);
618
+ }
619
+ var IconCal = () => import_react.default.createElement("svg", { width: 13, height: 13, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, "aria-hidden": "true" }, [
620
+ import_react.default.createElement("rect", { key: "r", x: 2.2, y: 3.4, width: 11.6, height: 10.4, rx: 2 }),
621
+ import_react.default.createElement("path", { key: "p", d: "M2.2 6.6h11.6M5.4 2.2v2.4M10.6 2.2v2.4" })
622
+ ]);
623
+ var IconClock = () => import_react.default.createElement("svg", { width: 13, height: 13, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, "aria-hidden": "true" }, [
624
+ import_react.default.createElement("circle", { key: "c", cx: 8, cy: 8, r: 5.8 }),
625
+ import_react.default.createElement("path", { key: "p", d: "M8 4.9V8l2.2 1.4" })
626
+ ]);
392
627
  function QuickAdd({ t, onAdd, busy }) {
393
628
  const [title, setTitle] = import_react.default.useState("");
394
629
  const [due, setDue] = import_react.default.useState("");
395
- const [priority, setPriority] = import_react.default.useState("normal");
630
+ const [dueTime, setDueTime] = import_react.default.useState("");
631
+ const [priority, setPriority] = import_react.default.useState("none");
632
+ const [parsed, setParsed] = import_react.default.useState(null);
633
+ const [manual, setManual] = import_react.default.useState(false);
634
+ const [flagOpen, setFlagOpen] = import_react.default.useState(false);
635
+ const dateRef = import_react.default.useRef(null);
636
+ const timeRef = import_react.default.useRef(null);
637
+ const openPicker = (ref) => {
638
+ const el = ref.current;
639
+ if (!el) return;
640
+ if (typeof el.showPicker === "function") {
641
+ try {
642
+ el.showPicker();
643
+ return;
644
+ } catch {
645
+ }
646
+ }
647
+ el.focus();
648
+ try {
649
+ el.click();
650
+ } catch {
651
+ }
652
+ };
653
+ import_react.default.useEffect(() => {
654
+ if (manual) return void 0;
655
+ const text = title;
656
+ if (!text || !text.trim()) {
657
+ setParsed(null);
658
+ return void 0;
659
+ }
660
+ const timer = setTimeout(() => {
661
+ const result = parseNaturalDue(text, Date.now());
662
+ if (result.matched.length) {
663
+ setParsed(result);
664
+ setDue(result.due ?? "");
665
+ setDueTime(result.dueTime ?? "");
666
+ } else {
667
+ setParsed(null);
668
+ }
669
+ }, 300);
670
+ return () => clearTimeout(timer);
671
+ }, [title, manual]);
396
672
  const submit = () => {
397
673
  if (busy) return;
398
- Promise.resolve(onAdd({ title, due, priority })).then((ok) => {
399
- if (ok !== false) setTitle("");
674
+ let payloadTitle = title;
675
+ if (!manual) {
676
+ const result = parseNaturalDue(title, Date.now());
677
+ if (result.matched.length) payloadTitle = result.title;
678
+ }
679
+ Promise.resolve(onAdd({
680
+ title: payloadTitle,
681
+ due: due || null,
682
+ dueTime: dueTime || null,
683
+ priority
684
+ })).then((ok) => {
685
+ if (ok !== false) {
686
+ setTitle("");
687
+ setParsed(null);
688
+ setManual(false);
689
+ setFlagOpen(false);
690
+ }
400
691
  }).catch(() => {
401
692
  });
402
693
  };
403
- return import_react.default.createElement("div", {
404
- style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", marginBottom: 10 }
405
- }, [
406
- import_react.default.createElement("input", {
407
- key: "ti",
408
- className: "wt-input",
409
- style: { flex: "1 1 200px", fontSize: 12.5, padding: "5px 9px" },
410
- value: title,
411
- placeholder: t("addPlaceholder"),
412
- "aria-label": t("addPlaceholder"),
413
- onChange: (event) => setTitle(event.target.value),
414
- onKeyDown: (event) => {
415
- if (event.key === "Enter") submit();
416
- }
417
- }),
418
- import_react.default.createElement("label", {
419
- key: "due",
420
- title: t("dueLabel"),
421
- style: { display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, color: C.label3 }
422
- }, [
423
- t("dueLabel"),
694
+ const today = dayKey2(Date.now());
695
+ const dateLabel = due ? due === today ? t("todayWord") : `${due.slice(5).replace("-", "-")}` : t("dateUnset");
696
+ const timeLabel = dueTime || t("timeUnset");
697
+ const prioLabel = priority === "high" ? t("prioHigh") : priority === "normal" ? t("prioNormal") : priority === "low" ? t("prioLow") : t("prioNone");
698
+ const prioColor = priority === "high" ? C.error : priority === "normal" ? C.warn : C.label3;
699
+ const flags = [
700
+ { value: "none", label: t("prioNone"), color: C.label3, dashed: true },
701
+ { value: "high", label: t("prioHigh"), color: C.error },
702
+ { value: "normal", label: t("prioNormal"), color: C.warn },
703
+ { value: "low", label: t("prioLow"), color: C.label3 }
704
+ ];
705
+ const setDate = (value) => {
706
+ setManual(true);
707
+ setDue(value);
708
+ if (!value) setDueTime("");
709
+ setFlagOpen(false);
710
+ };
711
+ const setTime = (value) => {
712
+ setManual(true);
713
+ if (value && !due) setDue(dayKey2(Date.now()));
714
+ setDueTime(value);
715
+ };
716
+ return import_react.default.createElement("div", { key: "qa", style: { marginBottom: 12 } }, [
717
+ // ---- 第一行:内容 + 添加 ----
718
+ import_react.default.createElement("div", { key: "l1", style: { display: "flex", alignItems: "center", gap: 8 } }, [
424
719
  import_react.default.createElement("input", {
425
- key: "i",
720
+ key: "ti",
426
721
  className: "wt-input",
427
- type: "date",
428
- value: due,
429
- "aria-label": t("dueLabel"),
430
- onChange: (event) => setDue(event.target.value)
431
- })
722
+ style: { flex: "1 1 auto", fontSize: 13, padding: "7px 10px" },
723
+ value: title,
724
+ placeholder: t("addPlaceholder"),
725
+ "aria-label": t("addPlaceholder"),
726
+ onChange: (event) => {
727
+ const next = event.target.value;
728
+ setTitle(next);
729
+ if (!next.trim()) {
730
+ setManual(false);
731
+ setParsed(null);
732
+ }
733
+ },
734
+ onKeyDown: (event) => {
735
+ if (event.key === "Enter") submit();
736
+ }
737
+ }),
738
+ import_react.default.createElement("button", {
739
+ key: "add",
740
+ type: "button",
741
+ className: "wt-btn wt-btn-primary",
742
+ disabled: Boolean(busy),
743
+ onClick: submit
744
+ }, t("add"))
432
745
  ]),
433
- import_react.default.createElement("label", {
434
- key: "prio",
435
- title: t("priorityLabel"),
436
- style: { display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, color: C.label3 }
437
- }, [
438
- t("priorityLabel"),
439
- import_react.default.createElement("select", {
440
- key: "s",
441
- className: "wt-input",
442
- value: priority,
443
- "aria-label": t("priorityLabel"),
444
- onChange: (event) => setPriority(event.target.value)
746
+ // ---- 第二行(更小):日期 / 时间 / 优先级 ----
747
+ import_react.default.createElement("div", { key: "l2", style: { display: "flex", alignItems: "center", gap: 10, marginTop: 8, fontSize: 11.5, flexWrap: "wrap" } }, [
748
+ // 日期:label 里套一个透明的原生 date 输入,点它即弹系统日历
749
+ import_react.default.createElement("label", {
750
+ key: "due",
751
+ className: `wt-chip${parsed && parsed.due ? " is-parsed" : ""}`,
752
+ title: t("dueLabel"),
753
+ onClick: (event) => {
754
+ event.preventDefault();
755
+ openPicker(dateRef);
756
+ },
757
+ style: { position: "relative", cursor: "pointer", color: due ? C.label : C.label3, borderColor: parsed && parsed.due ? C.brand : void 0 }
445
758
  }, [
446
- import_react.default.createElement("option", { key: "high", value: "high" }, t("prioHigh")),
447
- import_react.default.createElement("option", { key: "normal", value: "normal" }, t("prioNormal")),
448
- import_react.default.createElement("option", { key: "low", value: "low" }, t("prioLow"))
759
+ import_react.default.createElement(IconCal, { key: "i" }),
760
+ import_react.default.createElement("span", { key: "v" }, due ? `${today === due ? t("todayWord") : due}${parsed && parsed.due ? ` \xB7 ${t("fromParsed")}` : ""}` : t("dateUnset")),
761
+ import_react.default.createElement("input", {
762
+ key: "in",
763
+ ref: dateRef,
764
+ type: "date",
765
+ value: due,
766
+ "aria-label": t("dueLabel"),
767
+ style: { position: "absolute", inset: 0, opacity: 0, border: 0, padding: 0, width: "100%", height: "100%", cursor: "pointer" },
768
+ onChange: (event) => setDate(event.target.value)
769
+ })
770
+ ]),
771
+ // 时间:同样用原生 time 输入;只选时间时自动补今天
772
+ import_react.default.createElement("label", {
773
+ key: "time",
774
+ className: `wt-chip${parsed && parsed.dueTime ? " is-parsed" : ""}`,
775
+ title: t("dueTimeLabel"),
776
+ onClick: (event) => {
777
+ event.preventDefault();
778
+ openPicker(timeRef);
779
+ },
780
+ style: { position: "relative", cursor: "pointer", color: dueTime ? C.label : C.label3, borderColor: parsed && parsed.dueTime ? C.brand : void 0 }
781
+ }, [
782
+ import_react.default.createElement(IconClock, { key: "i" }),
783
+ import_react.default.createElement("span", { key: "v" }, `${timeLabel}${parsed && parsed.dueTime ? ` \xB7 ${t("fromParsed")}` : ""}`),
784
+ import_react.default.createElement("input", {
785
+ key: "in",
786
+ ref: timeRef,
787
+ type: "time",
788
+ value: dueTime,
789
+ lang: "zh-CN",
790
+ "aria-label": t("dueTimeLabel"),
791
+ style: { position: "absolute", inset: 0, opacity: 0, border: 0, padding: 0, width: "100%", height: "100%", cursor: "pointer" },
792
+ onChange: (event) => setTime(event.target.value)
793
+ })
794
+ ]),
795
+ // 优先级:点开四个小旗
796
+ import_react.default.createElement("span", { key: "prio", style: { position: "relative" } }, [
797
+ import_react.default.createElement("button", {
798
+ key: "btn",
799
+ type: "button",
800
+ className: "wt-chip",
801
+ style: { color: prioColor, cursor: "pointer", font: "inherit", fontSize: 11.5 },
802
+ title: t("priorityLabel"),
803
+ "aria-expanded": flagOpen,
804
+ onClick: () => setFlagOpen((open) => !open)
805
+ }, [
806
+ import_react.default.createElement(IconFlag, { key: "f", dashed: priority === "none", filled: priority !== "none" }),
807
+ import_react.default.createElement("span", { key: "v" }, `${t("priorityLabel")} \xB7 ${prioLabel}`)
808
+ ]),
809
+ flagOpen ? import_react.default.createElement("div", {
810
+ key: "pop",
811
+ style: {
812
+ position: "absolute",
813
+ top: "100%",
814
+ left: 0,
815
+ zIndex: 5,
816
+ marginTop: 4,
817
+ padding: 6,
818
+ display: "flex",
819
+ gap: 4,
820
+ background: C.layer1,
821
+ border: `1px solid ${C.border}`,
822
+ borderRadius: 10,
823
+ boxShadow: "0 8px 24px rgba(0,0,0,.12)"
824
+ }
825
+ }, flags.map((flag) => import_react.default.createElement("button", {
826
+ key: flag.value,
827
+ type: "button",
828
+ className: "wt-chip",
829
+ style: { color: flag.color, font: "inherit", fontSize: 11.5, cursor: "pointer", borderColor: priority === flag.value ? flag.color : void 0 },
830
+ title: flag.label,
831
+ onClick: () => {
832
+ setPriority(flag.value);
833
+ setFlagOpen(false);
834
+ }
835
+ }, [
836
+ import_react.default.createElement(IconFlag, { key: "f", filled: priority === flag.value, dashed: flag.value === "none" }),
837
+ import_react.default.createElement("span", { key: "v" }, flag.label)
838
+ ]))) : null
449
839
  ])
450
- ]),
451
- import_react.default.createElement("button", {
452
- key: "add",
453
- type: "button",
454
- className: "wt-btn wt-btn-primary",
455
- disabled: Boolean(busy),
456
- onClick: submit
457
- }, t("add"))
840
+ ])
458
841
  ]);
459
842
  }
460
- function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
843
+ function TodoRow({ todo, today, t, onToggle, onRemove, onSave, drag }) {
461
844
  const [editing, setEditing] = import_react.default.useState(false);
462
845
  const [draft, setDraft] = import_react.default.useState(null);
463
846
  const [notesOpen, setNotesOpen] = import_react.default.useState(false);
@@ -477,6 +860,7 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
477
860
  due,
478
861
  priority: row.priority === "high" || row.priority === "low" ? row.priority : "normal",
479
862
  project: typeof row.project === "string" ? row.project : "",
863
+ dueTime: typeof row.dueTime === "string" ? row.dueTime : "",
480
864
  notes: typeof row.notes === "string" ? row.notes : ""
481
865
  });
482
866
  setNotesOpen(Boolean(row.notes));
@@ -492,7 +876,12 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
492
876
  const title = String(draft.title ?? "").trim();
493
877
  if (title !== (row.title ?? "")) patch.title = title;
494
878
  const nextDue = draft.due || "";
495
- if (nextDue !== due) patch.due = nextDue;
879
+ if (nextDue !== due) {
880
+ patch.due = nextDue;
881
+ if (!nextDue) patch.dueTime = "none";
882
+ }
883
+ const nextDueTime = String(draft.dueTime ?? "").trim();
884
+ if (nextDue && nextDueTime !== (row.dueTime ?? "")) patch.dueTime = nextDueTime === "" ? "none" : nextDueTime;
496
885
  const priority = draft.priority || "normal";
497
886
  if (priority !== (row.priority ?? "normal")) patch.priority = priority;
498
887
  const project = String(draft.project ?? "").trim();
@@ -551,6 +940,15 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
551
940
  "aria-label": t("dueLabel"),
552
941
  onChange: (event) => setDraft((prev) => ({ ...prev, due: event.target.value }))
553
942
  })),
943
+ field("tm", t("dueTimeLabel"), import_react.default.createElement("input", {
944
+ className: "wt-input",
945
+ style: inputStyle,
946
+ type: "time",
947
+ value: draft.dueTime,
948
+ disabled: !draft.due,
949
+ "aria-label": t("dueTimeLabel"),
950
+ onChange: (event) => setDraft((prev) => ({ ...prev, dueTime: event.target.value }))
951
+ })),
554
952
  field("prio", t("priorityLabel"), import_react.default.createElement("select", {
555
953
  className: "wt-input",
556
954
  style: inputStyle,
@@ -627,7 +1025,39 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
627
1025
  title: row.source === "agent" ? t("sourceAgent") : t("sourceSession")
628
1026
  }, row.source === "agent" ? t("sourceAgent") : t("sourceSession")));
629
1027
  }
630
- return import_react.default.createElement("div", { className: "wt-row" }, [
1028
+ const dragAttrs = drag ? {
1029
+ draggable: true,
1030
+ onDragStart: (event) => {
1031
+ try {
1032
+ event.dataTransfer?.setData("text/plain", row.id ?? "");
1033
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
1034
+ } catch {
1035
+ }
1036
+ drag.onStart?.(row.id);
1037
+ },
1038
+ onDragOver: (event) => {
1039
+ event.preventDefault();
1040
+ drag.onOver?.(row.id);
1041
+ },
1042
+ onDrop: (event) => {
1043
+ event.preventDefault();
1044
+ drag.onDrop?.(row.id);
1045
+ },
1046
+ onDragEnd: () => drag.onEnd?.(),
1047
+ title: drag.hint,
1048
+ style: drag.dragging ? { opacity: 0.45 } : void 0
1049
+ } : {};
1050
+ return import_react.default.createElement("div", { className: "wt-row", ...dragAttrs, style: row.priority === "high" ? { borderLeft: `2px solid ${C.error}`, paddingLeft: 8 } : void 0 }, [
1051
+ /* 优先级呈现(设计稿 v5):高=红色左缘 + 实心红旗;中=琥珀;低=灰;无=什么都不显示 */
1052
+ row.priority && row.priority !== "none" ? import_react.default.createElement("span", {
1053
+ key: "prio-flag",
1054
+ title: row.priority === "high" ? t("prioHigh") : row.priority === "normal" ? t("prioNormal") : t("prioLow"),
1055
+ style: {
1056
+ display: "inline-flex",
1057
+ flex: "0 0 auto",
1058
+ color: row.priority === "high" ? C.error : row.priority === "normal" ? C.warn : C.label3
1059
+ }
1060
+ }, import_react.default.createElement(IconFlag, { filled: row.priority === "high" })) : null,
631
1061
  import_react.default.createElement("button", {
632
1062
  key: "check",
633
1063
  type: "button",
@@ -674,7 +1104,7 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
674
1104
  }, confirming ? t("confirmDelete") : import_react.default.createElement(IconTrash, { key: "i" }))
675
1105
  ]);
676
1106
  }
677
- function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave }) {
1107
+ function Group({ label, items, meta, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave, drag }) {
678
1108
  const list = asArray(items);
679
1109
  if (!list.length) return null;
680
1110
  return import_react.default.createElement("div", { style: { marginTop: 6 } }, [
@@ -688,7 +1118,7 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
688
1118
  }, [
689
1119
  import_react.default.createElement(IconChevron, { key: "c", open: !closed }),
690
1120
  import_react.default.createElement("span", { key: "l", style: { fontWeight: 600, fontSize: 12 } }, label),
691
- import_react.default.createElement("span", { key: "n", style: { fontSize: 11, color: C.label3 } }, String(list.length)),
1121
+ import_react.default.createElement("span", { key: "n", style: { fontSize: 11, color: C.label3 } }, meta || String(list.length)),
692
1122
  import_react.default.createElement("span", { key: "sp", style: { flex: 1 } }),
693
1123
  import_react.default.createElement("span", { key: "h", style: { fontSize: 11, color: C.label3 } }, closed ? t("expand") : t("collapse"))
694
1124
  ]),
@@ -699,7 +1129,15 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
699
1129
  t,
700
1130
  onToggle: onToggleTodo,
701
1131
  onRemove,
702
- onSave
1132
+ onSave,
1133
+ drag: drag ? {
1134
+ dragging: drag.dragId === todo?.id,
1135
+ hint: drag.hint,
1136
+ onStart: drag.onStart,
1137
+ onOver: drag.onOver,
1138
+ onDrop: drag.onDrop,
1139
+ onEnd: drag.onEnd
1140
+ } : void 0
703
1141
  })))
704
1142
  ]);
705
1143
  }
@@ -747,7 +1185,13 @@ var GROUPS = [
747
1185
  ];
748
1186
  function WorkTodoPanel(props) {
749
1187
  const t = typeof props?.t === "function" ? props.t : (key) => zh[key] ?? key;
750
- const { status, data, error, refresh } = useList();
1188
+ const [sort, setSortState] = import_react.default.useState(lastSort);
1189
+ const setSort = import_react.default.useCallback((next) => {
1190
+ lastSort = next;
1191
+ setSortState(next);
1192
+ }, []);
1193
+ const [dragId, setDragId] = import_react.default.useState(null);
1194
+ const { status, data, error, refresh } = useList(sort);
751
1195
  const [barError, setBarError] = import_react.default.useState(null);
752
1196
  const [dismissedReadError, setDismissedReadError] = import_react.default.useState(null);
753
1197
  const readError = error && error !== dismissedReadError ? error : null;
@@ -755,6 +1199,7 @@ function WorkTodoPanel(props) {
755
1199
  const [notice, setNotice] = import_react.default.useState(null);
756
1200
  const [pending, setPending] = import_react.default.useState(0);
757
1201
  const [closed, setClosed] = import_react.default.useState({ completed: true });
1202
+ const [view, setView] = import_react.default.useState("due");
758
1203
  const runWrite = import_react.default.useCallback(async (job) => {
759
1204
  setPending((n) => n + 1);
760
1205
  try {
@@ -784,6 +1229,30 @@ function WorkTodoPanel(props) {
784
1229
  const handleToggle = import_react.default.useCallback((ref) => runWrite(() => postJson("/status", { ref, toggle: true })), [runWrite]);
785
1230
  const handleRemove = import_react.default.useCallback((ref) => runWrite(() => postJson("/remove", { ref })), [runWrite]);
786
1231
  const handleSave = import_react.default.useCallback((ref, patch) => runWrite(() => postJson("/update", { ref, ...patch })), [runWrite]);
1232
+ const handleDropOn = import_react.default.useCallback((targetId) => {
1233
+ const dragging = dragId;
1234
+ setDragId(null);
1235
+ if (!dragging || !targetId || dragging === targetId) return Promise.resolve(false);
1236
+ const ids = asArray(data?.todos).map((todo) => todo?.id).filter(Boolean);
1237
+ const from = ids.indexOf(dragging);
1238
+ if (from === -1) return Promise.resolve(false);
1239
+ ids.splice(from, 1);
1240
+ const to = ids.indexOf(targetId);
1241
+ ids.splice(to === -1 ? ids.length : to, 0, dragging);
1242
+ return runWrite(() => postJson("/reorder", { ids })).then((result) => result.ok);
1243
+ }, [dragId, data, runWrite]);
1244
+ const dragHandlers = import_react.default.useMemo(() => ({
1245
+ dragId,
1246
+ hint: t("dragHint"),
1247
+ onStart: (id) => {
1248
+ setDragId(id);
1249
+ if (sort !== "manual") setSort("manual");
1250
+ },
1251
+ onOver: () => {
1252
+ },
1253
+ onDrop: (id) => handleDropOn(id),
1254
+ onEnd: () => setDragId(null)
1255
+ }), [dragId, sort, t, setSort, handleDropOn]);
787
1256
  const handleAdopt = import_react.default.useCallback((sessionTodos) => runWrite(() => postJson("/adopt", { todos: sessionTodos })).then((result) => {
788
1257
  if (result.ok) {
789
1258
  const created = Array.isArray(result.data?.created) ? result.data.created.length : 0;
@@ -832,9 +1301,18 @@ function WorkTodoPanel(props) {
832
1301
  const stats = data?.stats ?? {};
833
1302
  const today = typeof data?.today === "string" && data.today ? data.today : localToday();
834
1303
  const store = data?.store ?? null;
1304
+ const version = typeof data?.version === "string" ? data.version : "";
1305
+ const update = data?.update ?? null;
835
1306
  const num = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
836
1307
  const overdueCount = num(stats.overdue);
837
- const totalVisible = GROUPS.reduce((sum, group) => sum + asArray(groups[group.key]).length, 0);
1308
+ const projectGroups = asArray(data?.projectGroups);
1309
+ const viewGroups = view === "project" ? projectGroups.map((bucket) => ({
1310
+ key: `p:${bucket?.key ?? ""}`,
1311
+ label: bucket?.project ? String(bucket.project) : t("noProject"),
1312
+ meta: `${num(bucket?.open)}/${num(bucket?.total)}${num(bucket?.overdue) ? ` \xB7 ${t("statOverdue")} ${num(bucket.overdue)}` : ""}`,
1313
+ items: bucket?.todos
1314
+ })) : GROUPS.map((group) => ({ key: group.key, label: t(group.label), items: groups[group.key] }));
1315
+ const totalVisible = viewGroups.reduce((sum, group) => sum + asArray(group.items).length, 0);
838
1316
  const chips = [
839
1317
  { key: "open", label: t("statOpen"), value: num(stats.open) },
840
1318
  { key: "overdue", label: t("statOverdue"), value: overdueCount, alert: overdueCount > 0 },
@@ -858,6 +1336,31 @@ function WorkTodoPanel(props) {
858
1336
  import_react.default.createElement("span", { key: "l", style: { color: C.label3 } }, chip.label),
859
1337
  import_react.default.createElement("b", { key: "v", style: { fontWeight: 650 } }, String(chip.value))
860
1338
  ])),
1339
+ import_react.default.createElement("span", { key: "sort", className: "wt-seg", title: t("sortLabel") }, [
1340
+ ...["manual", "priority", "created"].map((mode) => import_react.default.createElement("button", {
1341
+ key: mode,
1342
+ type: "button",
1343
+ className: `wt-seg-btn${sort === mode ? " is-on" : ""}`,
1344
+ title: mode === "manual" ? t("dragHint") : `${t("sortLabel")}\uFF1A${t(`sort${mode === "manual" ? "Manual" : mode === "priority" ? "Priority" : "Created"}`)}`,
1345
+ onClick: () => setSort(mode)
1346
+ }, t(`sort${mode === "manual" ? "Manual" : mode === "priority" ? "Priority" : "Created"}`)))
1347
+ ]),
1348
+ import_react.default.createElement("span", { key: "view", className: "wt-seg" }, [
1349
+ import_react.default.createElement("button", {
1350
+ key: "due",
1351
+ type: "button",
1352
+ className: `wt-seg-btn${view === "due" ? " is-on" : ""}`,
1353
+ title: t("viewDue"),
1354
+ onClick: () => setView("due")
1355
+ }, t("viewDue")),
1356
+ import_react.default.createElement("button", {
1357
+ key: "proj",
1358
+ type: "button",
1359
+ className: `wt-seg-btn${view === "project" ? " is-on" : ""}`,
1360
+ title: t("viewProject"),
1361
+ onClick: () => setView("project")
1362
+ }, t("viewProject"))
1363
+ ]),
861
1364
  import_react.default.createElement("span", { key: "sp", style: { flex: 1 } }),
862
1365
  import_react.default.createElement("button", {
863
1366
  key: "rf",
@@ -938,17 +1441,19 @@ function WorkTodoPanel(props) {
938
1441
  }) : null,
939
1442
  import_react.default.createElement(QuickAdd, { key: "quick", t, busy: pending > 0, onAdd: handleAdd }),
940
1443
  /* 分组列表 */
941
- totalVisible ? import_react.default.createElement("div", { key: "groups" }, GROUPS.map((group) => import_react.default.createElement(Group, {
1444
+ totalVisible ? import_react.default.createElement("div", { key: "groups" }, viewGroups.map((group) => import_react.default.createElement(Group, {
942
1445
  key: group.key,
943
- label: t(group.label),
944
- items: groups[group.key],
1446
+ label: group.label,
1447
+ meta: group.meta,
1448
+ items: group.items,
945
1449
  closed: Boolean(closed[group.key]),
946
1450
  t,
947
1451
  today,
948
1452
  onToggleOpen: () => setClosed((prev) => ({ ...prev, [group.key]: !prev[group.key] })),
949
1453
  onToggleTodo: handleToggle,
950
1454
  onRemove: handleRemove,
951
- onSave: handleSave
1455
+ onSave: handleSave,
1456
+ drag: dragHandlers
952
1457
  }))) : import_react.default.createElement("div", {
953
1458
  key: "empty",
954
1459
  style: { padding: "22px 12px", textAlign: "center", color: C.label3 }
@@ -960,11 +1465,30 @@ function WorkTodoPanel(props) {
960
1465
  import_react.default.createElement("div", {
961
1466
  key: "foot",
962
1467
  style: { marginTop: 14, paddingTop: 8, borderTop: `1px solid ${C.border1}`, fontSize: 10.5, color: C.label3, lineHeight: 1.7 }
963
- }, store && store.location ? import_react.default.createElement("div", {
964
- key: "store",
965
- title: `${t("storeAt")}\uFF1A${store.location}`,
966
- style: { wordBreak: "break-all" }
967
- }, `${t("storeAt")} \xB7 ${store.kind === "domain" ? t("storeDomain") : t("storeFile")} \xB7 ${store.location}`) : null)
1468
+ }, [
1469
+ /* 角落信息:装了哪一版 + 有没有新版(一眼可见,不占主区) */
1470
+ import_react.default.createElement("div", {
1471
+ key: "ver",
1472
+ style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", marginBottom: 2 }
1473
+ }, [
1474
+ import_react.default.createElement("span", {
1475
+ key: "v",
1476
+ title: t("versionHint"),
1477
+ style: { color: C.label3 }
1478
+ }, `${t("versionLabel")} ${version ? `v${version}` : "\u2014"}`),
1479
+ update?.hasUpdate ? import_react.default.createElement("span", {
1480
+ key: "u",
1481
+ className: "wt-chip",
1482
+ style: { fontSize: 10, color: C.ok, borderColor: tint(C.ok, 50), background: tint(C.ok, 12) },
1483
+ title: `${t("updateHint")}\uFF1Adsh plugin --profile desktop add dsh-worktodo@${update.latest}`
1484
+ }, `${t("updateAvailable")} v${update.latest}`) : null
1485
+ ]),
1486
+ store && store.location ? import_react.default.createElement("div", {
1487
+ key: "store",
1488
+ title: `${t("storeAt")}\uFF1A${store.location}`,
1489
+ style: { wordBreak: "break-all" }
1490
+ }, `${t("storeAt")} \xB7 ${store.kind === "domain" ? t("storeDomain") : t("storeFile")} \xB7 ${store.location}`) : null
1491
+ ])
968
1492
  ]);
969
1493
  }
970
1494
  var inject = ["slots", "locale"];