@starwish-ai/xingyuan-dsh 0.5.9 → 0.6.1
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 +62 -0
- package/README.en-US.md +1 -0
- package/README.md +2 -0
- package/lib/client/cards.js +3 -1
- package/lib/client/chart-labels.js +4 -5
- package/lib/client/i18n.js +25 -12
- package/lib/client/pages/wishes.js +72 -33
- package/lib/client/styles.js +13 -5
- package/lib/client/ui.js +7 -0
- package/lib/client.js +119 -49
- package/lib/growth.js +3 -1
- package/lib/preset/charts.js +11 -19
- package/lib/preset/prompts.js +23 -6
- package/lib/preset/tools.js +198 -33
- package/lib/routes/api.js +4 -0
- package/lib/routes/pages-html.js +2 -2
- package/lib/store.js +82 -22
- package/lib/types/client/chart-labels.d.ts +1 -1
- package/lib/types/client/i18n.d.ts +13 -7
- package/lib/types/client/pages/types.d.ts +4 -0
- package/lib/types/client/pages/wishes.d.ts +1 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/client/ui.d.ts +2 -0
- package/lib/types/events.d.ts +10 -0
- package/lib/types/store.d.ts +36 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,68 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.1] - 2026-09-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Wishes page: collapsible task area**: each wish card's task list now starts collapsed
|
|
12
|
+
behind a full-row disclosure toggle ("N 个任务" / "Tasks (N)" with a chevron that flips
|
|
13
|
+
when expanded). Card-header progress, the "N to claim" badge and the wrap-up guidance
|
|
14
|
+
line stay visible while collapsed; expanding restores the task rows exactly as before —
|
|
15
|
+
claim / check-in buttons and the inline per-task detail expansion now nest inside the
|
|
16
|
+
card-level disclosure. Achieved wishes collapse the same way; wishes without tasks keep
|
|
17
|
+
their existing hint and get no toggle. Expanded cards are remembered across view-tab
|
|
18
|
+
switches (view-state snapshot). Client-only: no API, storage or write-path changes.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Wish-card copy de-jargoned: the empty hint now says "还没有任务" (was "暂无下属任务")
|
|
22
|
+
and the delete-wish confirmation says "它的任务…" (was "下属任务…"), matching the
|
|
23
|
+
established plain-wording standard for user-facing surfaces.
|
|
24
|
+
|
|
25
|
+
## [0.6.0] - 2026-09-05
|
|
26
|
+
|
|
27
|
+
> **Note:** behavioral revision — long-term ratios (wish progress, progress charts) join
|
|
28
|
+
> the commitment semantics: unclaimed tasks no longer count in the denominator, and
|
|
29
|
+
> achievement additionally requires zero unclaimed tasks. No data format change; upgrades
|
|
30
|
+
> from 0.5.x are seamless — the switch can only raise progress, and every newly-qualifying
|
|
31
|
+
> wish carries unclaimed candidates that the achievement gate holds back (nothing archives
|
|
32
|
+
> by accident).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **Achievement gate and the "wrapping up" state**: a wish achieves only at 100% progress
|
|
36
|
+
with no unclaimed tasks (single formula `wishProgressFromAgg` shared by the write path and
|
|
37
|
+
the read side). Full progress with unclaimed tasks parks the wish at "wrapping up" — the
|
|
38
|
+
bar is full, the wish is not archived — and the card shows the pending count with the
|
|
39
|
+
guidance line "claim to continue, or delete to achieve". The call is always the user's:
|
|
40
|
+
the assistant never claims or deletes on its own to force achievement.
|
|
41
|
+
- **Gate flips always reach the model**: check-in / claim / update / cancel / delete / create
|
|
42
|
+
(including batches) compare before/after wish views and, on achieve / wrap-up / rollback,
|
|
43
|
+
say so in the tool reply and re-emit the wish event, so chat card, reply and wishes page
|
|
44
|
+
stay in sync. Count drift without a flip also re-emits (the "N to claim" badge never
|
|
45
|
+
freezes).
|
|
46
|
+
- **Single-source display bits**: `pendingCount` / `settled` / `planning` / `achieved` are
|
|
47
|
+
produced by `freshWishes` and delivered via `/api/wishes` and `xingyuan/wish` events;
|
|
48
|
+
replays of older events degrade honestly to a plain progress display.
|
|
49
|
+
- **Wording standard**: all user-facing and verbatim-relay surfaces (i18n, tool replies,
|
|
50
|
+
prompt guides, standalone pages) say "待领取的任务" / "待收尾" — no internal jargon.
|
|
51
|
+
Locked by a banned-word test (`test/prompts.test.ts`) and a shared guidance-line constant.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- `wishProgress` and `taskCompletionRate` now divide by claimed-task days only; the
|
|
55
|
+
subtitle is fixed to "已领取任务" (the conditional "含未领取任务" wording is gone —
|
|
56
|
+
one metric, one meaning).
|
|
57
|
+
- Growth-page achievement counts and the `wishAchievement` chart consume the derived
|
|
58
|
+
achieved bit instead of `progress >= 100`, matching the wish card exactly.
|
|
59
|
+
- The session-opening overview reads fresh derived views (not the stored `archived` bit)
|
|
60
|
+
and distinguishes "all wishes achieved" from "no wishes yet".
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- Creating a bare wish showed "进度 0%" on the chat card while the wishes page said
|
|
64
|
+
"计划中" (the create event dropped the derived bits).
|
|
65
|
+
- Claiming a standalone task (no parent wish) claimed its days were folded into "wish
|
|
66
|
+
progress".
|
|
67
|
+
- Batch-delete replies glued the achievement guidance onto the summary line; batch paths
|
|
68
|
+
no longer re-scan the task table per wish on the after side.
|
|
69
|
+
|
|
8
70
|
## [0.5.9] - 2026-09-04
|
|
9
71
|
|
|
10
72
|
> **Note:** this release migrates the plugin to DeepSeek Harness `0.1.2-rc.1`
|
package/README.en-US.md
CHANGED
|
@@ -12,6 +12,7 @@ XingYuan is a wish & habit-building companion, shipped as a DeepSeek Harness plu
|
|
|
12
12
|
|
|
13
13
|
- **Conversation-driven**: create wishes / tasks in natural language with duplicate detection
|
|
14
14
|
- **Opportunity-day check-ins**: check-in, make-up, cancel, and future pre-checks; progress always recomputed against today
|
|
15
|
+
- **Commitment-based progress**: only claimed tasks count; a wish achieves when everything committed is done and nothing is left to claim — full progress with unclaimed tasks leaves it "wrapping up" for your call (claim to continue, or delete to achieve), never nagging, never deciding for you
|
|
15
16
|
- **Micro-actions**: break an overwhelming goal into 3–7 tiny steps and walk through them guided
|
|
16
17
|
- **Growth system**: levels Lv.1–Lv.10, streak bonuses, stat cards, 30-day bar chart
|
|
17
18
|
- **Session view tabs**: Today / Wishes / Tasks / Calendar / Growth / Memory — buttons call action endpoints directly; shown only in XingYuan-preset sessions by default, switchable to always show/hide and per-tab in Settings
|
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
- **对话驱动**:自然语言创建愿望 / 任务,自动查重相似项
|
|
14
14
|
- **机会日打卡**:周期任务打卡、补卡、取消、未来预勾,进度跨天恒为最新
|
|
15
|
+
- **承诺口径进度**:只有领取的任务算进度;达成 = 承诺全部完成且没有待领取的任务——
|
|
16
|
+
做完但有没领的,愿望停在「待收尾」等你决定(领了继续,或删掉就达成),不催也不代劳
|
|
15
17
|
- **微行动拆解**:把无从下手的目标拆成 3–7 个小步,引导式逐个完成或跳过
|
|
16
18
|
- **成长体系**:等级 Lv.1–Lv.10、连续加成、统计卡与近 30 天柱状图
|
|
17
19
|
- **会话视图页**:今日 / 愿望 / 任务 / 日历 / 成长 / 记忆 六个标签页,页面按钮直连动作接口;
|
package/lib/client/cards.js
CHANGED
|
@@ -22,9 +22,11 @@ function WishView(props) {
|
|
|
22
22
|
style: categoryVars(wish.wish.colorKey, wish.wish.categoryName),
|
|
23
23
|
}, t('badge.wish')), createElement('span', { className: 'xy-title' }, deleted ? t('state.deleted', { title: wish.wish.title }) : wish.wish.title)),
|
|
24
24
|
// 已删除卡不再渲染活体进度(删除态 × 进度百分比自相矛盾)
|
|
25
|
+
// 三态位(achieved/planning/settled/pendingCount)来自事件载荷(wishProgressFromAgg 派生),
|
|
26
|
+
// 与愿望页/工具回包同一称呼;旧事件无这些位则如实降级为纯「进度 X%」显示(不重建谓词,§5.2 规则 7)
|
|
25
27
|
createElement('div', { className: 'xy-meta' }, deleted
|
|
26
28
|
? wish.wish.categoryName
|
|
27
|
-
: `${wish.wish.categoryName} · ${t('wish.progress', { percent: wish.wish.progress })}`));
|
|
29
|
+
: `${wish.wish.categoryName} · ${wish.wish.achieved ? t('wish.achieved') : wish.wish.planning ? t('wish.planning') : t('wish.progress', { percent: wish.wish.progress })}${wish.wish.pendingCount ? ` · ${t('wish.pendingCount', { n: wish.wish.pendingCount })}` : ''}${wish.wish.settled ? ` · ${t('wish.settled')}` : ''}`));
|
|
28
30
|
}
|
|
29
31
|
function TaskView(props) {
|
|
30
32
|
const t = useXyT();
|
|
@@ -81,12 +81,11 @@ export const CHART_SUBTITLE_KEYS = {
|
|
|
81
81
|
'按任务 TopN': 'chart.subtitle.byTaskTopN',
|
|
82
82
|
'按愿望 TopN': 'chart.subtitle.byWishTopN',
|
|
83
83
|
'按实际打卡时刻分桶;过滤范围为打卡日区间': 'chart.subtitle.byHourBucket',
|
|
84
|
-
'
|
|
85
|
-
'%(应打天数完成率,含未领取任务)': 'chart.subtitle.progressPercentPending',
|
|
84
|
+
'%(已领取任务完成率)': 'chart.subtitle.claimedPercent',
|
|
86
85
|
};
|
|
87
86
|
/**
|
|
88
87
|
* 副标题本地化:固定短语查表;含数字的模板(近 N 天 / A/B 天 / 当前 A 天 / 最长 B 天 /
|
|
89
|
-
*
|
|
88
|
+
* 已领取任务口径)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
|
|
90
89
|
*/
|
|
91
90
|
export function localizeChartSubtitle(subtitle) {
|
|
92
91
|
const fixed = CHART_SUBTITLE_KEYS[subtitle];
|
|
@@ -95,9 +94,9 @@ export function localizeChartSubtitle(subtitle) {
|
|
|
95
94
|
let m = /^近 (\d+) 天$/.exec(subtitle);
|
|
96
95
|
if (m !== null)
|
|
97
96
|
return t('chart.subtitle.lastDays', { n: m[1] });
|
|
98
|
-
m = /^(\d+)\/(\d+)
|
|
97
|
+
m = /^(\d+)\/(\d+) 天(已领取任务)$/.exec(subtitle);
|
|
99
98
|
if (m !== null)
|
|
100
|
-
return t('chart.subtitle.
|
|
99
|
+
return t('chart.subtitle.daysClaimed', { done: m[1], due: m[2] });
|
|
101
100
|
m = /^(\d+)\/(\d+) 天$/.exec(subtitle);
|
|
102
101
|
if (m !== null)
|
|
103
102
|
return t('chart.subtitle.daysRatio', { done: m[1], due: m[2] });
|
package/lib/client/i18n.js
CHANGED
|
@@ -58,11 +58,19 @@ export const ZH_DICT = {
|
|
|
58
58
|
'state.done': '已完成',
|
|
59
59
|
// --- 愿望卡 / 愿望页 ---
|
|
60
60
|
'wish.progress': '进度 {percent}%',
|
|
61
|
-
'wish.
|
|
61
|
+
'wish.planning': '计划中',
|
|
62
|
+
'wish.achieved': '已达成 🎉',
|
|
63
|
+
'wish.pendingCount': '{n} 个任务待领取',
|
|
64
|
+
'wish.settled': '待收尾',
|
|
65
|
+
'wish.settleHint': '答应自己的都做完了——待领取的领了继续,或删掉就达成',
|
|
62
66
|
'wish.eta': '预计完成 {date}',
|
|
63
|
-
'wish.noTasks': '
|
|
67
|
+
'wish.noTasks': '还没有任务——在对话里告诉我,让我为它推荐打卡任务',
|
|
68
|
+
'wish.tasksToggle': '{n} 个任务',
|
|
64
69
|
'wish.pageTitle': '我的愿望',
|
|
65
|
-
'wish.summary': '{active} 个进行中{
|
|
70
|
+
'wish.summary': '{active} 个进行中{planning}{settled}{achieved}',
|
|
71
|
+
'wish.planningSuffix': ' · {n} 个计划中',
|
|
72
|
+
'wish.settledSuffix': ' · {n} 个待收尾',
|
|
73
|
+
'wish.achievedSuffix': ' · {n} 个已达成',
|
|
66
74
|
'wish.sectionAchieved': '已达成',
|
|
67
75
|
'wish.empty.title': '还没有愿望',
|
|
68
76
|
'wish.empty.hint': '告诉我想实现什么,我来帮你拆解成可坚持的计划。',
|
|
@@ -121,7 +129,7 @@ export const ZH_DICT = {
|
|
|
121
129
|
'confirm.undoAt': '确定撤销「{name}」在{date}的打卡吗?进度将回退。',
|
|
122
130
|
'confirm.futureAt': '「{name}」的打卡日 {date} 在今天之后,提前打卡表示承诺当天完成。确认?',
|
|
123
131
|
'confirm.deleteTask': '确定删除任务「{name}」吗?其打卡记录与微行动拆解将一并删除,不可恢复。',
|
|
124
|
-
'confirm.deleteWish': '确定删除愿望「{name}
|
|
132
|
+
'confirm.deleteWish': '确定删除愿望「{name}」吗?它的任务、打卡记录与微行动拆解将一并删除,不可恢复。',
|
|
125
133
|
'confirm.renameCategory': '把分类「{old}」下的全部愿望改名为「{new}」吗?(共 {count} 个)', 'confirm.deleteEmptyCategory': '删除空分类「{name}」的自定义颜色吗?',
|
|
126
134
|
// --- 今日页 ---
|
|
127
135
|
'today.title': '今日打卡 · {date}',
|
|
@@ -343,10 +351,9 @@ export const ZH_DICT = {
|
|
|
343
351
|
'chart.subtitle.byTaskTopN': '按任务 TopN',
|
|
344
352
|
'chart.subtitle.byWishTopN': '按愿望 TopN',
|
|
345
353
|
'chart.subtitle.byHourBucket': '按实际打卡时刻分桶;过滤范围为打卡日区间',
|
|
346
|
-
'chart.subtitle.
|
|
347
|
-
'chart.subtitle.progressPercentPending': '%(应打天数完成率,含未领取任务)',
|
|
354
|
+
'chart.subtitle.claimedPercent': '%(已领取任务完成率)',
|
|
348
355
|
'chart.subtitle.lastDays': '近 {n} 天',
|
|
349
|
-
'chart.subtitle.
|
|
356
|
+
'chart.subtitle.daysClaimed': '{done}/{due} 天(已领取任务)',
|
|
350
357
|
'chart.subtitle.daysRatio': '{done}/{due} 天',
|
|
351
358
|
'chart.subtitle.streak': '当前 {current} 天 / 最长 {best} 天',
|
|
352
359
|
// --- 详情聚合视图 ---
|
|
@@ -480,11 +487,18 @@ const EN_DICT = {
|
|
|
480
487
|
'state.deleted': 'Deleted “{title}”',
|
|
481
488
|
'state.done': 'Done',
|
|
482
489
|
'wish.progress': 'Progress {percent}%',
|
|
483
|
-
'wish.
|
|
490
|
+
'wish.planning': 'Planning',
|
|
491
|
+
'wish.achieved': 'Achieved 🎉',
|
|
492
|
+
'wish.pendingCount': '{n} to claim',
|
|
493
|
+
'wish.settled': 'Wrapping up',
|
|
494
|
+
'wish.settleHint': 'Everything you committed to is done — claim the rest to keep going, or delete them to achieve the wish',
|
|
484
495
|
'wish.eta': 'Target date {date}',
|
|
485
496
|
'wish.noTasks': 'No tasks yet — tell me in chat and I will suggest check-in tasks for it',
|
|
497
|
+
'wish.tasksToggle': 'Tasks ({n})',
|
|
486
498
|
'wish.pageTitle': 'My Wishes',
|
|
487
|
-
'wish.summary': '{active} in progress{achieved}',
|
|
499
|
+
'wish.summary': '{active} in progress{planning}{settled}{achieved}',
|
|
500
|
+
'wish.planningSuffix': ' · {n} planning',
|
|
501
|
+
'wish.settledSuffix': ' · {n} wrapping up',
|
|
488
502
|
'wish.achievedSuffix': ' · {n} achieved',
|
|
489
503
|
'wish.sectionAchieved': 'Achieved',
|
|
490
504
|
'wish.empty.title': 'No wishes yet',
|
|
@@ -751,10 +765,9 @@ const EN_DICT = {
|
|
|
751
765
|
'chart.subtitle.byTaskTopN': 'By task (top N)',
|
|
752
766
|
'chart.subtitle.byWishTopN': 'By wish (top N)',
|
|
753
767
|
'chart.subtitle.byHourBucket': 'Bucketed by actual check-in time; filtered to the check-in date range',
|
|
754
|
-
'chart.subtitle.
|
|
755
|
-
'chart.subtitle.progressPercentPending': '% of scheduled-day completion (incl. unclaimed)',
|
|
768
|
+
'chart.subtitle.claimedPercent': '% of committed-task completion',
|
|
756
769
|
'chart.subtitle.lastDays': 'Last {n} days',
|
|
757
|
-
'chart.subtitle.
|
|
770
|
+
'chart.subtitle.daysClaimed': '{done}/{due} days (claimed tasks)',
|
|
758
771
|
'chart.subtitle.daysRatio': '{done}/{due} days',
|
|
759
772
|
'chart.subtitle.streak': 'Current {current}d / best {best}d',
|
|
760
773
|
'detail.grid.title': 'Check-in record',
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** 愿望页:进行中/已达成愿望卡 +
|
|
1
|
+
/** 愿望页:进行中/已达成愿望卡 + 任务区披露(默认收起,展开集合跨标签保留)+ 任务行(领取/打卡/详情聚合)+ 愿望删除 + 分类管理 + 快速新建。 */
|
|
2
2
|
import { createElement, useState, useSyncExternalStore } from 'react';
|
|
3
3
|
import { postAction } from '../api.js';
|
|
4
4
|
import { useXyT } from '../i18n.js';
|
|
5
5
|
import { softConfirmDanger, useActionGuard, usePageData, useScrollTopOnMount, useStableScrollbar } from '../hooks.js';
|
|
6
6
|
import { getViewState, setViewState } from '../view-state.js';
|
|
7
7
|
import { todayHintStore } from '../tab-hint.js';
|
|
8
|
-
import { PageEmpty, PageError, PageSkeleton, StaleBanner, toast, IconTrash, focusPageTitle } from '../ui.js';
|
|
8
|
+
import { PageEmpty, PageError, PageSkeleton, StaleBanner, toast, IconTrash, IconChevronDown, focusPageTitle } from '../ui.js';
|
|
9
9
|
import { categoryVars } from '../../category-color.js';
|
|
10
10
|
import { TaskLine } from './task-line.js';
|
|
11
11
|
import { formatMediumDate } from './format.js';
|
|
@@ -34,13 +34,30 @@ export function WishesPage() {
|
|
|
34
34
|
next.add(taskId);
|
|
35
35
|
setExpanded(next);
|
|
36
36
|
};
|
|
37
|
+
// 愿望卡任务区披露集合(默认收起):愿望页定位总览,任务明细是二级信息;打卡/领取
|
|
38
|
+
// 主触点在今日页/日历,卡头进度/待领取数/收尾指引不受收起影响。展开集合与任务详情
|
|
39
|
+
// 展开集分开(两个粒度互不覆盖),同样经 view-state 跨标签切换保留
|
|
40
|
+
const [tasksOpen, setTasksOpenRaw] = useState(() => getViewState('wishes.tasksOpen', new Set()));
|
|
41
|
+
const setTasksOpen = (next) => { setViewState('wishes.tasksOpen', next); setTasksOpenRaw(next); };
|
|
42
|
+
const toggleTasks = (wishId) => {
|
|
43
|
+
const next = new Set(tasksOpen);
|
|
44
|
+
if (next.has(wishId))
|
|
45
|
+
next.delete(wishId);
|
|
46
|
+
else
|
|
47
|
+
next.add(wishId);
|
|
48
|
+
setTasksOpen(next);
|
|
49
|
+
};
|
|
37
50
|
if (page.error !== undefined && page.data === undefined)
|
|
38
51
|
return createElement(PageError, { message: page.error, onRetry: () => void page.reload() });
|
|
39
52
|
const data = page.data;
|
|
40
53
|
if (data === undefined)
|
|
41
54
|
return createElement(PageSkeleton);
|
|
42
|
-
|
|
55
|
+
// 页头四态计数:进行中/计划中/待收尾/已达成各计各的,与卡头徽章同一称呼
|
|
56
|
+
// (消灭「摘要算进行中、卡上写计划中」的并见歧义);全部取服务端派生位,展示层不重建谓词
|
|
43
57
|
const achieved = data.wishes.filter((w) => w.archived);
|
|
58
|
+
const settledCount = data.wishes.filter((w) => w.settled).length;
|
|
59
|
+
const planningCount = data.wishes.filter((w) => !w.archived && w.planning).length;
|
|
60
|
+
const active = data.wishes.filter((w) => !w.archived);
|
|
44
61
|
/** 删除愿望(级联下属任务与打卡记录,服务端同一写路径):确认后直连动作并整页刷新。
|
|
45
62
|
* 行移除后触发按钮销毁、焦点落空——刷新完成后把焦点交给页面标题兜底。 */
|
|
46
63
|
const removeWish = (wish) => {
|
|
@@ -70,37 +87,59 @@ export function WishesPage() {
|
|
|
70
87
|
? [line, createElement(TaskDetailPanel, { key: `d-${task.taskId}`, id: `xy-task-detail-${task.taskId}`, taskId: task.taskId, today: data.today, onChanged: page.reload })]
|
|
71
88
|
: [line];
|
|
72
89
|
});
|
|
73
|
-
const card = (wish) =>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
className: 'xy-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
const card = (wish) => {
|
|
91
|
+
// 承诺口径(§5.2 规则 7):进度分母只计已领取任务。展示三态:
|
|
92
|
+
// 计划中(无已领取任务)/ 进行中(正常百分比)/ 待结算(满进度而有候选——不归档,附收尾指引)。
|
|
93
|
+
// settled/pendingCount/planning 直取服务端 wishProgressFromAgg 派生位(随 /api/wishes 下发),
|
|
94
|
+
// 展示层禁止重建 `progress>=100 && 有候选` 闸门或 planning 谓词(单一判定源,防与工具回包分叉)
|
|
95
|
+
const { pendingCount, settled, planning } = wish;
|
|
96
|
+
const progressLabel = planning ? t('wish.planning') : t('wish.progress', { percent: wish.progress });
|
|
97
|
+
const pendingMeta = pendingCount > 0 ? ` · ${t('wish.pendingCount', { n: pendingCount })}` : '';
|
|
98
|
+
return createElement('div', { key: wish.wishId, className: 'xy-wishcard' }, createElement('div', { className: 'xy-card-head' }, createElement('span', {
|
|
99
|
+
className: 'xy-badge xy-badge-cat',
|
|
100
|
+
style: categoryVars(wish.colorKey, wish.categoryName),
|
|
101
|
+
}, wish.categoryName), createElement('span', { className: 'xy-title' }, wish.title), createElement('span', { className: 'xy-progress-num' }, progressLabel, pendingCount > 0
|
|
102
|
+
? createElement('span', { className: 'xy-meta' }, pendingMeta)
|
|
103
|
+
: null), createElement('button', {
|
|
104
|
+
className: 'xy-btn xy-btn-danger xy-btn-icon',
|
|
105
|
+
disabled: deleting,
|
|
106
|
+
'aria-label': t('common.delete') + ' · ' + wish.title,
|
|
107
|
+
title: t('common.delete'),
|
|
108
|
+
onClick: () => removeWish(wish),
|
|
109
|
+
}, createElement(IconTrash))), wish.description !== undefined ? createElement('div', { className: 'xy-meta' }, wish.description) : null, wish.estimatedCompletionDate !== undefined
|
|
110
|
+
? createElement('div', { className: 'xy-meta' }, t('wish.eta', { date: formatMediumDate(wish.estimatedCompletionDate) }))
|
|
111
|
+
: null,
|
|
112
|
+
// 与今日页 hero 进度条同一语义语法:读屏可感知进度(role+valuenow),不只是一根哑条
|
|
113
|
+
createElement('div', {
|
|
114
|
+
className: 'xy-bar',
|
|
115
|
+
role: 'progressbar',
|
|
116
|
+
'aria-valuemin': 0,
|
|
117
|
+
'aria-valuemax': 100,
|
|
118
|
+
'aria-valuenow': wish.progress,
|
|
119
|
+
'aria-label': progressLabel + pendingMeta,
|
|
120
|
+
}, createElement('div', { className: 'xy-bar-fill', style: { transform: `scaleX(${Math.min(wish.progress, 100) / 100})` } })), settled ? createElement('div', { className: 'xy-meta' }, t('wish.settleHint')) : null,
|
|
121
|
+
// 任务区披露:有任务时默认收起为整行开关(文本=任务数 + 箭头,aria-controls 关联
|
|
122
|
+
// 容器 id;展开后开关仍在列表上方兼作收起控件);无任务维持原文案,不出开关
|
|
123
|
+
...(wish.tasks.length > 0
|
|
124
|
+
? [
|
|
125
|
+
createElement('button', {
|
|
126
|
+
key: 'wishtoggle',
|
|
127
|
+
className: 'xy-wishtoggle',
|
|
128
|
+
'aria-expanded': tasksOpen.has(wish.wishId),
|
|
129
|
+
'aria-controls': `xy-wishtasks-${wish.wishId}`,
|
|
130
|
+
onClick: () => toggleTasks(wish.wishId),
|
|
131
|
+
}, t('wish.tasksToggle', { n: wish.tasks.length }), createElement(IconChevronDown)),
|
|
132
|
+
tasksOpen.has(wish.wishId)
|
|
133
|
+
? createElement('div', { key: 'wishtasks', id: `xy-wishtasks-${wish.wishId}`, className: 'xy-wishtasks' }, ...wishTasks(wish))
|
|
134
|
+
: null,
|
|
135
|
+
]
|
|
136
|
+
: [createElement('div', { key: 'notasks', className: 'xy-meta' }, t('wish.noTasks'))]));
|
|
137
|
+
};
|
|
102
138
|
return createElement('div', { className: 'xy-page', ref: stabilize }, page.error !== undefined ? createElement(StaleBanner, { onRetry: () => void page.reload() }) : null, showNoPresetHint ? createElement('p', { className: 'xy-hint' }, t('today.noPresetHint')) : null, createElement('div', { className: 'xy-page-head' }, createElement('h2', { className: 'xy-page-title' }, t('wish.pageTitle')), createElement('span', { className: 'xy-meta' }, t('wish.summary', {
|
|
103
|
-
|
|
139
|
+
// 四态计数各计各的(进行中扣除计划中与待收尾,不相套);列表渲染仍归未完成组(未完成≠进行中计数)
|
|
140
|
+
active: active.length - settledCount - planningCount,
|
|
141
|
+
planning: planningCount > 0 ? t('wish.planningSuffix', { n: planningCount }) : '',
|
|
142
|
+
settled: settledCount > 0 ? t('wish.settledSuffix', { n: settledCount }) : '',
|
|
104
143
|
achieved: achieved.length > 0 ? t('wish.achievedSuffix', { n: achieved.length }) : '',
|
|
105
144
|
})),
|
|
106
145
|
// 动作组整体右置(单一 margin-left:auto),避免多按钮各自 auto margin 平分剩余空间
|
package/lib/client/styles.js
CHANGED
|
@@ -236,9 +236,9 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
236
236
|
}
|
|
237
237
|
.xy-btn:active:not(:disabled){transform:scale(.97)}
|
|
238
238
|
.xy-btn:disabled{opacity:.55;cursor:not-allowed}
|
|
239
|
-
.xy-btn:focus-visible,.xy-input:focus-visible,.xy-cell:focus-visible,.xy-seg-btn:focus-visible,.xy-toggle:focus-visible,.xy-swatch:focus-visible,.xy-growth-col:focus-visible{outline:2px solid var(--xyd-accent);outline-offset:2px}
|
|
239
|
+
.xy-btn:focus-visible,.xy-input:focus-visible,.xy-cell:focus-visible,.xy-seg-btn:focus-visible,.xy-toggle:focus-visible,.xy-swatch:focus-visible,.xy-growth-col:focus-visible,.xy-wishtoggle:focus-visible{outline:2px solid var(--xyd-accent);outline-offset:2px}
|
|
240
240
|
/* 触控基线:消除移动端双击缩放延迟与系统点按高亮(交互反馈统一由 hover/active 承担) */
|
|
241
|
-
.xy-btn,.xy-seg-btn,.xy-cell,.xy-swatch,.xy-toggle,.xy-growth-col{touch-action:manipulation;-webkit-tap-highlight-color:transparent}
|
|
241
|
+
.xy-btn,.xy-seg-btn,.xy-cell,.xy-swatch,.xy-toggle,.xy-growth-col,.xy-wishtoggle{touch-action:manipulation;-webkit-tap-highlight-color:transparent}
|
|
242
242
|
/* 行内按钮:≥26px 命中目标(卡片头/操作行的紧凑主战按钮) */
|
|
243
243
|
.xy-btn-inline{margin-left:auto;padding:3px 10px;font-size:12px;min-height:26px;display:inline-flex;align-items:center}
|
|
244
244
|
/* 图标幽灵键(≥26px 方形命中目标):编辑/删除这类高频重复的行内低频动作降噪——
|
|
@@ -321,9 +321,17 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
321
321
|
.xy-wishcard:hover{border-color:var(--dsw-alias-border-l2);box-shadow:var(--xyd-shadow-card)}
|
|
322
322
|
}
|
|
323
323
|
.xy-progress-num{margin-left:auto;flex:none;color:var(--dsw-alias-label-secondary);font-size:12px;font-variant-numeric:tabular-nums}
|
|
324
|
-
/*
|
|
324
|
+
/* 任务区披露开关(默认收起,展开集合走 view-state):整行按钮继承虚线分隔(原属
|
|
325
|
+
* .xy-wishtasks 的卡内分界),箭头旋转只动 transform;按钮文本含任务数(口语化,无状态词)。 */
|
|
326
|
+
.xy-wishtoggle{margin-top:8px;display:flex;align-items:center;gap:5px;width:100%;padding:8px 0 5px;background:none;border:0;border-top:1px dashed var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;text-align:left}
|
|
327
|
+
@media (hover:hover) and (pointer:fine){
|
|
328
|
+
.xy-wishtoggle:hover{color:var(--dsw-alias-label-primary)}
|
|
329
|
+
}
|
|
330
|
+
.xy-wishtoggle svg{flex:none;transition:transform .15s ease}
|
|
331
|
+
.xy-wishtoggle[aria-expanded='true'] svg{transform:rotate(180deg)}
|
|
332
|
+
/* 卡内子任务列表:分隔线已上移到披露开关,行留白由任务行自身 padding 承担。
|
|
325
333
|
* 结构为 [taskline, detail?, taskline, detail?] —— 相邻选择器精确命中「新任务行」边界。 */
|
|
326
|
-
.xy-wishtasks{
|
|
334
|
+
.xy-wishtasks{display:flex;flex-direction:column}
|
|
327
335
|
.xy-wishtasks>.xy-taskline+.xy-taskline,.xy-wishtasks>.xy-detail+.xy-taskline{border-top:1px solid var(--dsw-alias-border-l1)}
|
|
328
336
|
.xy-wishtasks>.xy-taskline{padding:9px 0}
|
|
329
337
|
/* 展开详情是卡内最后一个元素时收掉底部留白:卡片自身 padding 已承担收尾 */
|
|
@@ -581,6 +589,6 @@ body[data-ds-dark-theme] .xy-dcell-checked{color:var(--xyd-on-dcell)}
|
|
|
581
589
|
@media (prefers-reduced-motion: reduce){
|
|
582
590
|
.xy-skel,.xy-toast,.xy-modal-backdrop,.xy-modal{animation:none}
|
|
583
591
|
.xy-toast-out,.xy-modal-backdrop.xy-modal-out,.xy-modal-out .xy-modal{transition:none}
|
|
584
|
-
.xy-bar-fill,.xy-btn,.xy-toggle,.xy-toggle::after,.xy-wishcard,.xy-cell,.xy-daynum,.xy-seg-btn,.xy-input,.xy-swatch,.xy-grouprow{transition:none}
|
|
592
|
+
.xy-bar-fill,.xy-btn,.xy-toggle,.xy-toggle::after,.xy-wishcard,.xy-cell,.xy-daynum,.xy-seg-btn,.xy-input,.xy-swatch,.xy-grouprow,.xy-wishtoggle svg{transition:none}
|
|
585
593
|
}
|
|
586
594
|
`;
|
package/lib/client/ui.js
CHANGED
|
@@ -200,6 +200,13 @@ export function IconEdit() {
|
|
|
200
200
|
style: { display: 'block', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
201
201
|
}, createElement('path', { d: 'M13.5 5l5.5 5.5' }), createElement('path', { d: 'M5 19l1-4L17.5 3.5a2.12 2.12 0 0 1 3 3L9 18z' }));
|
|
202
202
|
}
|
|
203
|
+
/** 下箭头线性图标(披露开关方向指示;展开态旋转由宿主按钮的 aria-expanded 选择器驱动)。 */
|
|
204
|
+
export function IconChevronDown() {
|
|
205
|
+
return createElement('svg', {
|
|
206
|
+
viewBox: '0 0 24 24', width: 14, height: 14, 'aria-hidden': 'true', focusable: 'false',
|
|
207
|
+
style: { display: 'block', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
208
|
+
}, createElement('path', { d: 'M6 9l6 6 6-6' }));
|
|
209
|
+
}
|
|
203
210
|
// ===== 页面三态 =====
|
|
204
211
|
export function PageError(props) {
|
|
205
212
|
return createElement('div', { className: 'xy-page-center' }, createElement('div', { className: 'xy-empty-title' }, props.message), createElement('button', { className: 'xy-btn', onClick: props.onRetry }, props.retryLabel ?? t('common.retry')));
|