dsh-task-ledger 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -536,6 +536,26 @@ body[data-ds-dark-theme]{
536
536
  /* 过程正文:宽度受限、长行折行 */
537
537
  .dtl-worklog{max-width:72ch;font-size:var(--dtl-font-md);line-height:1.6}
538
538
 
539
+ /* 趋势分组柱状图:细柱(8px)+ 底部基线 + 均值虚线;「精细」靠细柱与参考线 */
540
+ .dtl-gchart{position:relative;display:flex;align-items:flex-end;gap:10px;height:104px;
541
+ border-bottom:1px solid var(--dtl-border);}
542
+ .dtl-gchart__col{display:flex;flex-direction:column;justify-content:flex-end;align-items:center;
543
+ flex:1 1 0;height:100%;min-width:0;}
544
+ .dtl-gchart__pair{display:flex;align-items:flex-end;gap:2px;height:100%;justify-content:center;}
545
+ .dtl-gchart__bar{width:8px;min-height:0;border-radius:var(--dtl-radius-xs) var(--dtl-radius-xs) 0 0;}
546
+ .dtl-gchart__bar--done{background:var(--dtl-business);}
547
+ .dtl-gchart__bar--created{background:var(--dtl-chip-bg);}
548
+ .dtl-gchart__col:hover .dtl-gchart__bar--done{background:var(--dtl-accent);}
549
+ .dtl-gchart__label{font-size:10px;line-height:14px;color:var(--dtl-fg-3);white-space:nowrap;}
550
+ .dtl-gchart__avg{position:absolute;left:0;right:0;border-top:1px dashed var(--dtl-border-strong);pointer-events:none;}
551
+ .dtl-gchart__avgtag{position:absolute;right:0;font-size:10px;line-height:12px;color:var(--dtl-fg-3);
552
+ background:var(--dtl-surface-1);padding:0 2px;transform:translateY(-100%);}
553
+ .dtl-gchart-legend{display:flex;align-items:center;gap:var(--dtl-space-3);font-size:var(--dtl-font-xs);color:var(--dtl-fg-2);}
554
+ .dtl-gchart-legend i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:4px;}
555
+
556
+ /* 过程正文:宽度受限、长行折行 */
557
+ .dtl-worklog{max-width:72ch;font-size:var(--dtl-font-md);line-height:1.6}
558
+
539
559
  /* 趋势柱状图:div 柱 + token 色,无新依赖 */
540
560
  .dtl-chart{display:flex;align-items:flex-end;gap:var(--dtl-space-2);height:120px;}
541
561
  .dtl-chart__col{display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:var(--dtl-space-1);flex:1 1 0;height:100%;min-width:0;}
@@ -619,19 +639,24 @@ body[data-ds-dark-theme]{
619
639
  }
620
640
  });
621
641
  }
622
- /** 统计卡:主数字 + 灰色口径小字;点一下联动下方列表筛选。 */
642
+ /** 统计卡:主数字 + 灰色口径小字;点一下联动下方列表筛选。
643
+ * `value` 为 0 时显示「—」(用户 2026-10-17 定稿:0 会被读成「没数据 / 出错了」);
644
+ * `hint` 可以是富文本(如把「停滞」两字标红);
645
+ * `interactive: false` = 纯展示卡(统计台:首页没有已结案可筛,不装可点)。 */
623
646
  function StatCard(props) {
624
647
  ensureUiBase();
625
- return (0, react.createElement)("button", {
626
- type: "button",
627
- className: "dtl-card dtl-card--clickable dtl-stat",
628
- "data-active": props.active,
648
+ const interactive = props.interactive !== false && props.onClick !== void 0;
649
+ const shown = props.value === 0 || props.value === "0" ? "—" : String(props.value);
650
+ return (0, react.createElement)(interactive ? "button" : "div", {
651
+ type: interactive ? "button" : void 0,
652
+ className: `dtl-card dtl-stat${interactive ? " dtl-card--clickable" : ""}`,
653
+ "data-active": props.active === true,
629
654
  title: props.title,
630
- onClick: props.onClick
655
+ onClick: interactive ? props.onClick : void 0
631
656
  }, (0, react.createElement)("span", { style: {
632
657
  fontSize: "var(--dtl-font-sm)",
633
658
  color: "var(--dtl-fg-2)"
634
- } }, props.label), (0, react.createElement)("span", { className: "dtl-stat__value" }, props.value), props.hint === void 0 || props.hint === "" ? null : (0, react.createElement)("span", { className: "dtl-stat__hint" }, props.hint));
659
+ } }, props.label), (0, react.createElement)("span", { className: "dtl-stat__value" }, shown), props.hint === void 0 || props.hint === null || props.hint === "" ? null : (0, react.createElement)("span", { className: "dtl-stat__hint" }, props.hint));
635
660
  }
636
661
  /** chip(分类 / 标签 / 工作区名)。 */
637
662
  function Chip(props) {
@@ -658,25 +683,41 @@ body[data-ds-dark-theme]{
658
683
  color: "var(--dtl-fg-2)"
659
684
  } }, props.hint));
660
685
  }
661
- /**
662
- * 柱状图(div 柱,无新依赖):每桶一柱,柱高按最大值归一;
663
- * 数值常显(10px 灰),具体时刻走 title。零值的桶画 2px 矮桩,避免「这一周不存在」的误读。
664
- */
665
- function BarChart(props) {
686
+ /** 分组柱状图(div 柱,无新依赖):完成 / 新增各一细柱,底部基线 + 完成数平均值虚线。
687
+ * 「精细」= 细柱 + 基线 + 均值线,不是粗柱堆高度(用户 2026-10-17)。 */
688
+ function GroupedBarChart(props) {
666
689
  ensureUiBase();
667
- const max = props.buckets.reduce((peak, bucket) => Math.max(peak, bucket.value), 0);
668
- return (0, react.createElement)("div", {
669
- className: "dtl-chart",
690
+ const plotHeight = 104;
691
+ const max = props.weeks.reduce((peak, week) => Math.max(peak, week.done, week.created), 0);
692
+ const doneTotal = props.weeks.reduce((sum, week) => sum + week.done, 0);
693
+ const avg = props.weeks.length === 0 ? 0 : doneTotal / props.weeks.length;
694
+ const barHeight = (value) => max === 0 ? 0 : Math.round(value / max * plotHeight);
695
+ return (0, react.createElement)("div", { style: {
696
+ position: "relative",
697
+ paddingBottom: "18px"
698
+ } }, (0, react.createElement)("div", {
699
+ className: "dtl-gchart",
670
700
  role: "img",
671
701
  "aria-label": props.ariaLabel
672
- }, props.buckets.map((bucket) => (0, react.createElement)("div", {
673
- key: bucket.label,
674
- className: "dtl-chart__col",
675
- title: bucket.title
676
- }, (0, react.createElement)("span", { className: "dtl-chart__val" }, bucket.value === 0 ? "" : String(bucket.value)), (0, react.createElement)("span", {
677
- className: "dtl-chart__bar",
678
- style: { height: `${max === 0 ? 2 : Math.max(2, Math.round(bucket.value / max * 100))}%` }
679
- }), (0, react.createElement)("span", { className: "dtl-chart__label" }, bucket.label))));
702
+ }, avg > 0 ? (0, react.createElement)("div", {
703
+ className: "dtl-gchart__avg",
704
+ style: { bottom: `${barHeight(avg)}px` }
705
+ }, (0, react.createElement)("span", { className: "dtl-gchart__avgtag" }, props.avgLabel)) : null, props.weeks.map((week) => (0, react.createElement)("div", {
706
+ key: week.label,
707
+ className: "dtl-gchart__col",
708
+ title: week.title
709
+ }, (0, react.createElement)("div", { className: "dtl-gchart__pair" }, (0, react.createElement)("span", {
710
+ className: "dtl-gchart__bar dtl-gchart__bar--done",
711
+ style: { height: `${barHeight(week.done)}px` }
712
+ }), (0, react.createElement)("span", {
713
+ className: "dtl-gchart__bar dtl-gchart__bar--created",
714
+ style: { height: `${barHeight(week.created)}px` }
715
+ })), (0, react.createElement)("span", { className: "dtl-gchart__label" }, week.label)))));
716
+ }
717
+ /** 图例(完成 / 新增 两个色块)。 */
718
+ function ChartLegend(props) {
719
+ ensureUiBase();
720
+ return (0, react.createElement)("div", { className: "dtl-gchart-legend" }, (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { background: "var(--dtl-business)" } }), props.doneLabel), (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { background: "var(--dtl-chip-bg)" } }), props.createdLabel));
680
721
  }
681
722
  //#endregion
682
723
  //#region src/client/task-row.ts
@@ -875,18 +916,29 @@ body[data-ds-dark-theme]{
875
916
  fontSize: "var(--dtl-font-xl)",
876
917
  fontWeight: 600
877
918
  };
878
- /** 统计区:左 2/3 卡网格 + 右 1/3 趋势图(窄屏堆叠)。 */
879
- const statsAreaStyle = {
919
+ /** 操作台:全宽 4 列。列数**写死 4**,不用 auto-fit——那会排出 3+1 的非对称
920
+ * (用户 2026-10-17:必须上下对称;窄屏由 statsRowStyle 的堆叠兜底,仍是 2+2)。 */
921
+ const cardsGridStyle = {
922
+ display: "grid",
923
+ gridTemplateColumns: "repeat(4,minmax(0,1fr))",
924
+ gap: "var(--dtl-space-3)"
925
+ };
926
+ /** 第二行:统计台(2×2)+ 趋势图(右 1/3)。 */
927
+ const statsRowStyle = {
880
928
  display: "grid",
881
929
  gridTemplateColumns: "minmax(0,2fr) minmax(0,1fr)",
882
- gap: "var(--dtl-space-3)",
883
- marginBottom: "var(--dtl-space-4)"
930
+ gap: "var(--dtl-space-3)"
884
931
  };
885
- const cardsGridStyle = {
932
+ const statsCardsStyle = {
886
933
  display: "grid",
887
- gridTemplateColumns: "repeat(auto-fit,minmax(148px,1fr))",
934
+ gridTemplateColumns: "repeat(2,minmax(0,1fr))",
888
935
  gap: "var(--dtl-space-3)"
889
936
  };
937
+ const regionCaptionStyle = {
938
+ fontSize: "var(--dtl-font-xs)",
939
+ color: "var(--dtl-fg-3)",
940
+ margin: "var(--dtl-space-4) 0 var(--dtl-space-2)"
941
+ };
890
942
  const chartCardStyle = {
891
943
  display: "flex",
892
944
  flexDirection: "column",
@@ -959,14 +1011,34 @@ body[data-ds-dark-theme]{
959
1011
  setQuery(patch);
960
1012
  }
961
1013
  });
962
- const chartBuckets = (0, react.useMemo)(() => (board?.closed.weekly ?? []).map((bucket) => ({
1014
+ /** 统计台卡:不可点(首页没有已结案可筛),hover 给具体日期区间。 */
1015
+ const periodCard = (label, period) => (0, react.createElement)(StatCard, {
1016
+ label: t(label),
1017
+ value: period?.done ?? 0,
1018
+ hint: t("statCreatedHint", { n: period?.created ?? 0 }),
1019
+ interactive: false,
1020
+ title: period === void 0 ? void 0 : t("periodRange", {
1021
+ from: period.from,
1022
+ to: period.to
1023
+ })
1024
+ });
1025
+ const [chartScope, setChartScope] = (0, react.useState)("all");
1026
+ const chartWeeks = (0, react.useMemo)(() => (board === null ? [] : board.closed.weekly[chartScope]).map((bucket) => ({
963
1027
  label: bucket.week.slice(5),
964
- value: bucket.count,
1028
+ done: bucket.done,
1029
+ created: bucket.created,
965
1030
  title: t("chartBucket", {
966
1031
  week: bucket.week,
967
- n: bucket.count
1032
+ done: bucket.done,
1033
+ created: bucket.created
968
1034
  })
969
- })), [board, t]);
1035
+ })), [
1036
+ board,
1037
+ chartScope,
1038
+ t
1039
+ ]);
1040
+ const chartDoneTotal = chartWeeks.reduce((sum, week) => sum + week.done, 0);
1041
+ const chartEmpty = chartWeeks.every((week) => week.done === 0 && week.created === 0);
970
1042
  return (0, react.createElement)("div", { style: pageStyle }, (0, react.createElement)("div", { style: {
971
1043
  display: "flex",
972
1044
  justifyContent: "center"
@@ -1009,42 +1081,73 @@ body[data-ds-dark-theme]{
1009
1081
  onClick: () => {
1010
1082
  refresh();
1011
1083
  }
1012
- }, state.refreshing ? t("refreshing") : t("refresh")))), tab === "stats" ? (0, react.createElement)("div", { style: hintStyle }, t("statsPlaceholder")) : (0, react.createElement)("div", null, stats === void 0 ? null : (0, react.createElement)("div", { style: statsAreaStyle }, (0, react.createElement)("div", { style: cardsGridStyle }, card("statMyActive", String(stats.myActive), t("statMyActiveHint", { n: stats.myBlocked }), {
1084
+ }, state.refreshing ? t("refreshing") : t("refresh")))), tab === "stats" ? (0, react.createElement)("div", { style: hintStyle }, t("statsPlaceholder")) : (0, react.createElement)("div", null, stats === void 0 ? null : (0, react.createElement)("div", { style: cardsGridStyle }, card("statMyActive", stats.myActive, t("statMyActiveHint", { n: stats.myBlocked }), {
1013
1085
  scope: "mine",
1014
1086
  status: "all",
1015
1087
  workspace: ""
1016
- }), card("statOpen", String(stats.open), t("statOpenHint", { n: days(stats.oldestOpenDays) }), {
1088
+ }), card("statOpen", stats.open, t("statOpenHint", { n: days(stats.oldestOpenDays) }), {
1017
1089
  scope: "all",
1018
1090
  status: "open",
1019
1091
  workspace: ""
1020
- }), card("statDoing", String(stats.doing), t("statDoingHint", { n: stats.doing - stats.myActive + stats.myBlocked }), {
1092
+ }), card("statDoing", stats.doing, t("statDoingHint", { n: stats.doing - stats.myActive + stats.myBlocked }), {
1021
1093
  scope: "all",
1022
1094
  status: "active",
1023
1095
  workspace: ""
1024
- }), card("statBlocked", String(stats.blocked), t("statBlockedHint", { n: days(stats.longestBlockedDays) }), {
1025
- scope: "all",
1026
- status: "blocked",
1027
- workspace: ""
1028
- }), card("statWeekDone", String(board?.closed.weekDone ?? 0), t("statWeekDoneHint"), {
1029
- scope: "all",
1030
- status: "all",
1031
- workspace: ""
1032
- }), card("statStale", String(stats.stale), t("statStaleHint", { n: stats.staleDays }), {
1033
- scope: "all",
1034
- status: "stale",
1035
- workspace: ""
1036
- })), (0, react.createElement)("div", {
1096
+ }), (0, react.createElement)(StatCard, {
1097
+ label: t("statBlockedStale"),
1098
+ value: stats.blocked,
1099
+ hint: stats.stale === 0 ? void 0 : (0, react.createElement)("span", null, (0, react.createElement)("span", { style: {
1100
+ color: "var(--dtl-danger)",
1101
+ fontWeight: 600
1102
+ } }, t("statStaleWord")), ` ${t("statStaleHint", { n: stats.stale })}`),
1103
+ active: isActive(state.query, {
1104
+ scope: "all",
1105
+ status: "blocked",
1106
+ workspace: ""
1107
+ }),
1108
+ title: t("statStaleHint", { n: stats.staleDays }),
1109
+ onClick: () => {
1110
+ setQuery({
1111
+ scope: "all",
1112
+ status: "blocked",
1113
+ workspace: ""
1114
+ });
1115
+ }
1116
+ })), (0, react.createElement)("div", { style: regionCaptionStyle }, t("tabStats")), (0, react.createElement)("div", { style: statsRowStyle }, (0, react.createElement)("div", { style: statsCardsStyle }, periodCard("statWeekDone", board?.trend.thisWeek), periodCard("statLastWeekDone", board?.trend.lastWeek), periodCard("statMonthDone", board?.trend.thisMonth), periodCard("statLastMonthDone", board?.trend.lastMonth)), (0, react.createElement)("div", {
1037
1117
  className: "dtl-card",
1038
1118
  style: chartCardStyle
1039
1119
  }, (0, react.createElement)("div", { style: {
1120
+ display: "flex",
1121
+ alignItems: "center",
1122
+ justifyContent: "space-between",
1123
+ gap: "var(--dtl-space-2)"
1124
+ } }, (0, react.createElement)("span", { style: {
1040
1125
  fontSize: "var(--dtl-font-md)",
1041
1126
  fontWeight: 600
1042
- } }, t("chartTitle")), chartBuckets.every((bucket) => bucket.value === 0) ? (0, react.createElement)("div", { style: {
1127
+ } }, t("chartTitle")), (0, react.createElement)(Segmented, {
1128
+ id: "dtl-chart-scope",
1129
+ value: chartScope,
1130
+ label: t("scopeLabel"),
1131
+ size: "sm",
1132
+ variant: "inset",
1133
+ items: [{
1134
+ value: "all",
1135
+ label: t("chartScopeAll")
1136
+ }, {
1137
+ value: "mine",
1138
+ label: t("chartScopeMine")
1139
+ }],
1140
+ onChange: setChartScope
1141
+ })), chartEmpty ? (0, react.createElement)("div", { style: {
1043
1142
  ...hintStyle,
1044
1143
  padding: "var(--dtl-space-4) 0"
1045
- } }, t("chartEmpty")) : (0, react.createElement)(BarChart, {
1046
- buckets: chartBuckets,
1047
- ariaLabel: t("chartTitle")
1144
+ } }, t("chartEmpty")) : (0, react.createElement)(GroupedBarChart, {
1145
+ weeks: chartWeeks,
1146
+ ariaLabel: t("chartTitle"),
1147
+ avgLabel: t("chartAvg", { n: Math.round(chartDoneTotal / Math.max(1, chartWeeks.length) * 10) / 10 })
1148
+ }), (0, react.createElement)(ChartLegend, {
1149
+ doneLabel: t("chartLegendDone"),
1150
+ createdLabel: t("chartLegendCreated")
1048
1151
  }), (0, react.createElement)("div", { style: {
1049
1152
  fontSize: "var(--dtl-font-xs)",
1050
1153
  color: "var(--dtl-fg-3)"
@@ -1209,18 +1312,26 @@ body[data-ds-dark-theme]{
1209
1312
  statOpenHint: "最老一条躺了 {n}",
1210
1313
  statDoing: "进行中",
1211
1314
  statDoingHint: "别人在做 {n}",
1212
- statBlocked: "阻塞中",
1213
- statBlockedHint: "卡住最久 {n}",
1315
+ statBlockedStale: "阻塞 · 停滞",
1316
+ statStaleWord: "停滞",
1317
+ statStaleHint: "{n} 条",
1214
1318
  statWeekDone: "本周完成",
1215
- statWeekDoneHint: "按结案时间",
1216
- statStale: "停滞任务",
1217
- statStaleHint: "超过 {n} 天没动",
1319
+ statLastWeekDone: "上周完成",
1320
+ statMonthDone: "本月完成",
1321
+ statLastMonthDone: "上月完成",
1322
+ statCreatedHint: "新增 {n} 条",
1218
1323
  daysNone: "—",
1219
1324
  daysValue: "{n} 天",
1220
- chartTitle: "近 8 周完成",
1221
- chartCaption: "按结案时间、周一起桶;悬停看该周条数",
1222
- chartBucket: "{week} 那一周:完成 {n} 条",
1223
- chartEmpty: "近 8 周没有已结案记录",
1325
+ chartTitle: "近 8 周",
1326
+ chartCaption: "完成(按结案时间)/ 新增(按创建时间),周一起桶;我的 = owner 是我",
1327
+ chartBucket: "{week} 那一周:完成 {done} 条,新增 {created} 条",
1328
+ chartEmpty: "近 8 周没有记录",
1329
+ chartScopeAll: "所有",
1330
+ chartScopeMine: "我的",
1331
+ chartLegendDone: "完成",
1332
+ chartLegendCreated: "新增",
1333
+ chartAvg: "周均 {n}",
1334
+ periodRange: "{from} ~ {to}",
1224
1335
  scopeLabel: "范围",
1225
1336
  scopeMine: "我的",
1226
1337
  scopeAll: "全部工作区",
@@ -1277,18 +1388,26 @@ body[data-ds-dark-theme]{
1277
1388
  statOpenHint: "oldest waiting {n}",
1278
1389
  statDoing: "In progress",
1279
1390
  statDoingHint: "{n} by others",
1280
- statBlocked: "Blocked",
1281
- statBlockedHint: "stuck for {n}",
1391
+ statBlockedStale: "Blocked · Stale",
1392
+ statStaleWord: "Stale",
1393
+ statStaleHint: "{n}",
1282
1394
  statWeekDone: "Done this week",
1283
- statWeekDoneHint: "by close time",
1284
- statStale: "Stale tasks",
1285
- statStaleHint: "no movement for {n}+ days",
1395
+ statLastWeekDone: "Done last week",
1396
+ statMonthDone: "Done this month",
1397
+ statLastMonthDone: "Done last month",
1398
+ statCreatedHint: "{n} created",
1286
1399
  daysNone: "—",
1287
1400
  daysValue: "{n}d",
1288
1401
  chartTitle: "Last 8 weeks",
1289
- chartCaption: "by close time, weeks start Monday; hover a bar for its count",
1290
- chartBucket: "week of {week}: {n} done",
1291
- chartEmpty: "Nothing closed in the last 8 weeks",
1402
+ chartCaption: "done (by close time) / created (by create time), weeks start Monday; mine = owner is me",
1403
+ chartBucket: "week of {week}: {done} done, {created} created",
1404
+ chartEmpty: "Nothing in the last 8 weeks",
1405
+ chartScopeAll: "All",
1406
+ chartScopeMine: "Mine",
1407
+ chartLegendDone: "Done",
1408
+ chartLegendCreated: "Created",
1409
+ chartAvg: "avg {n}/wk",
1410
+ periodRange: "{from} ~ {to}",
1292
1411
  scopeLabel: "Scope",
1293
1412
  scopeMine: "Mine",
1294
1413
  scopeAll: "All workspaces",