@starwish-ai/xingyuan-dsh 0.5.5 → 0.5.6
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 +83 -0
- package/README.en-US.md +1 -1
- package/README.md +1 -1
- package/lib/client/api.js +8 -1
- package/lib/client/i18n.js +34 -25
- package/lib/client/pages/growth.js +3 -2
- package/lib/client/pages/memory.js +17 -3
- package/lib/client/pages/task-line.js +28 -9
- package/lib/client/pages/tasks.js +2 -0
- package/lib/client/pages/today.js +6 -1
- package/lib/client/styles.js +23 -12
- package/lib/client.js +96 -49
- package/lib/domain.js +68 -1
- package/lib/growth.js +9 -9
- package/lib/opportunity.js +9 -2
- package/lib/preset/prompts.js +8 -4
- package/lib/preset/tools.js +5 -5
- package/lib/routes/api.js +6 -10
- package/lib/sqlite.js +10 -2
- package/lib/store.js +20 -12
- package/lib/types/client/api.d.ts +1 -1
- package/lib/types/client/i18n.d.ts +20 -13
- package/lib/types/client/pages/task-line.d.ts +4 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/domain.d.ts +9 -0
- package/lib/types/opportunity.d.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,89 @@ 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.5.6] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
> **Note:** this release is the follow-up to the v0.5.5 six-perspective quality
|
|
11
|
+
> pass: the same six reviews re-ran against 0.5.5 and every accepted finding is
|
|
12
|
+
> fixed here — one data-safety gate, honesty fixes in the growth ladder, focus
|
|
13
|
+
> management on the highest-frequency action, and a batch of contrast, copy and
|
|
14
|
+
> localization closures. No data format changes; existing databases and recorded
|
|
15
|
+
> sessions replay as before.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Write-path schema gate** (`makeXingyuanStore`): the storage domain validates
|
|
19
|
+
records only when cold-opening the medium — a write path that skipped a field
|
|
20
|
+
check (e.g. an over-long task name from the model) used to persist silently and
|
|
21
|
+
then brick the next startup (`invalid-record` refuses to open, plugin dead until
|
|
22
|
+
manual surgery). Every `put`/`update`/`global.set` is now guarded by the declared
|
|
23
|
+
zod schema at the single store assembly point and rejected at write time with a
|
|
24
|
+
stable `invalid_record` code; valid records parse as identity, reads pay nothing.
|
|
25
|
+
Locked by `test/domain-guard.test.ts`.
|
|
26
|
+
- **Network-failure copy**: connection-level fetch failures (dsh restarting) no
|
|
27
|
+
longer surface the browser's raw English (`Failed to fetch` / `Load failed`) in
|
|
28
|
+
the Chinese UI; they map to a localized "network request failed, retry" message.
|
|
29
|
+
- **Task rows keep keyboard focus after group-migrating actions**: a successful
|
|
30
|
+
check-in moves the row between the Today page's open/done lists and destroyed the
|
|
31
|
+
pressed button, dropping focus to `<body>`; focus now falls back to the page
|
|
32
|
+
title (the same pattern as undo). The same treatment covers a check-in that
|
|
33
|
+
closes a task anywhere task rows render, and claiming a task on the Tasks page,
|
|
34
|
+
where the row moves between status groups.
|
|
35
|
+
- **Memory page**: the search empty-state no longer flashes between keystrokes
|
|
36
|
+
while the debounce is pending, a settled zero-hit search now shows the "no
|
|
37
|
+
matches" copy instead of the "no memories yet" empty-library one (the server's
|
|
38
|
+
`total` is query-filtered, so the two were indistinguishable before), and the
|
|
39
|
+
key field's server minimum (2 characters) is validated inline with its guidance
|
|
40
|
+
message instead of a generic error.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- **Level rewards describe what exists**: the growth ladder no longer promises
|
|
44
|
+
avatar frames, task templates or style gating that the product does not have
|
|
45
|
+
(coach styles were always free in Settings); rewards are now the honorary title
|
|
46
|
+
itself, capped by the Lv.10 pinnacle. Server table and both dictionaries updated
|
|
47
|
+
together (the Chinese dictionary is locked to the server table by
|
|
48
|
+
`test/growth.test.ts`).
|
|
49
|
+
- **Levels read "荣誉" not "权益"** on the Growth page hero and in `get_growth_stats`
|
|
50
|
+
output, matching the honorary rewards.
|
|
51
|
+
- **Today plan keeps the achieved-closing row**: checking the last opportunity day
|
|
52
|
+
closes the task, which used to remove the row from the Today page entirely (the
|
|
53
|
+
counter shrank right after checking in). A closed-achieved task with a check-in
|
|
54
|
+
on that date stays in the done group with its undo entry; cancelling the check-in
|
|
55
|
+
revives the task through the existing freshness path.
|
|
56
|
+
- **Calendar hover preserves state colors**: hovering a partially/fully-completed
|
|
57
|
+
day no longer washes the semantic amber/green chip with the accent hover tint —
|
|
58
|
+
the state color is exactly what the hover was inspecting.
|
|
59
|
+
- **Contrast closures**: micro-action card secondary text and skipped steps use the
|
|
60
|
+
`label-on-2` pair and a higher muted opacity so both themes stay above the
|
|
61
|
+
4.5:1 line (the same lift applies to deleted wish cards in chat); calendar/
|
|
62
|
+
check-in-cell state foreground colors moved into the theme token region as
|
|
63
|
+
paired `--xyd-on-c2` / `--xyd-on-c3` / `--xyd-on-dcell` tokens.
|
|
64
|
+
- **Date validation accepts only real calendar dates**: `2026-02-30` used to pass
|
|
65
|
+
the format check (`Date.parse` rolls it over instead of returning NaN) and then
|
|
66
|
+
silently produce a task with an empty opportunity sequence — or land in the
|
|
67
|
+
check-in table as a fake day that skews the streak replay. Task due dates, wish
|
|
68
|
+
estimated dates and check-in target dates (including the any-date back-fill on
|
|
69
|
+
no-deadline tasks) now share the opportunity calculator's semantic `isIsoDate`
|
|
70
|
+
round-trip check, and the routes' duplicate implementation converges onto it.
|
|
71
|
+
- **Prompt coherence**: the check-in example moved out of the "no-confirm mode"
|
|
72
|
+
section into confirm mode (check-in is a confirm-gated write) and no longer
|
|
73
|
+
demonstrates a streak number the tool does not return; list-tool outputs tell the
|
|
74
|
+
model to use bracketed IDs only for follow-up calls, not to render them to the
|
|
75
|
+
user; `generate_chart`'s `wishId` parameter names the camelCase chart keys.
|
|
76
|
+
- **Memory search bar wraps** so the saved notice is not clipped off narrow panels.
|
|
77
|
+
- **Copy/wording**: the Settings preference-unavailable hint no longer hardcodes a
|
|
78
|
+
stale count; the English "clear all memories" dialog is grammatical for one item;
|
|
79
|
+
the Growth page EXP/experience units are consistent in Chinese; an unknown level
|
|
80
|
+
falls back to a localized name instead of an empty label; the 404 API error and
|
|
81
|
+
the wish-not-found tool/route error carry localizable codes; the English
|
|
82
|
+
check-in grid summary is grammatical for one cell; README wording aligned with
|
|
83
|
+
the UI ("连续加成", straight quotes in English).
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- **sqlite backend**: a corrupted table row now reports `malformed-medium` like the
|
|
87
|
+
global slot does, instead of a raw `SyntaxError` with a split troubleshooting path.
|
|
88
|
+
- **Mock generator**: the detail-panel fixture in `debug/gen-mock.ts` takes an id
|
|
89
|
+
parameter so the wish/task scenarios no longer emit duplicate DOM ids.
|
|
90
|
+
|
|
8
91
|
## [0.5.5] - 2026-08-28
|
|
9
92
|
|
|
10
93
|
> **Note:** this release is a six-perspective quality pass (product, visual design,
|
package/README.en-US.md
CHANGED
|
@@ -28,7 +28,7 @@ Tools are only mounted on sessions using the XingYuan preset; other sessions are
|
|
|
28
28
|
dsh plugin --profile web add @starwish-ai/xingyuan-dsh
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
After starting the Web GUI, installation succeeded when
|
|
31
|
+
After starting the Web GUI, installation succeeded when "星愿" (XingYuan) appears in the agent picker.
|
|
32
32
|
|
|
33
33
|
## Data & Backup
|
|
34
34
|
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- **对话驱动**:自然语言创建愿望 / 任务,自动查重相似项
|
|
14
14
|
- **机会日打卡**:周期任务打卡、补卡、取消、未来预勾,进度跨天恒为最新
|
|
15
15
|
- **微行动拆解**:把无从下手的目标拆成 3–7 个小步,引导式逐个完成或跳过
|
|
16
|
-
- **成长体系**:等级 Lv.1–Lv.10
|
|
16
|
+
- **成长体系**:等级 Lv.1–Lv.10、连续加成、统计卡与近 30 天柱状图
|
|
17
17
|
- **会话视图页**:今日 / 愿望 / 任务 / 日历 / 成长 / 记忆 六个标签页,页面按钮直连动作接口;
|
|
18
18
|
默认仅星愿预设的会话显示,可在设置里切换「始终显示 / 始终隐藏」并按标签勾选
|
|
19
19
|
- **记忆**:重要记忆自动注入上下文(上限可配),支持增删改查
|
package/lib/client/api.js
CHANGED
|
@@ -40,8 +40,15 @@ const ERROR_KEY = {
|
|
|
40
40
|
bad_coach_style: 'err.bad_coach_style',
|
|
41
41
|
bad_interests: 'err.bad_interests',
|
|
42
42
|
};
|
|
43
|
-
/**
|
|
43
|
+
/** 网络层失败的浏览器原文是英文技术串(Chrome "Failed to fetch"、Safari "Load failed"、
|
|
44
|
+
* Firefox "NetworkError…"、undici "fetch failed"),直出会打破本地化承诺——识别后统一
|
|
45
|
+
* 转本地化文案。仅匹配 TypeError(fetch 在请求发出前失败的规范类型)中的已知模式,
|
|
46
|
+
* 其余 TypeError(代码缺陷)仍原样抛出便于排障。 */
|
|
47
|
+
const NETWORK_ERROR_PATTERN = /failed to fetch|load failed|networkerror|fetch failed/i;
|
|
48
|
+
/** 把任意抛错转成用户可读文案:网络失败/ActionError 按键本地化;其余直出消息。 */
|
|
44
49
|
export function describeError(e) {
|
|
50
|
+
if (e instanceof TypeError && NETWORK_ERROR_PATTERN.test(e.message))
|
|
51
|
+
return t('common.networkError');
|
|
45
52
|
if (!(e instanceof ActionError))
|
|
46
53
|
return e instanceof Error ? e.message : String(e);
|
|
47
54
|
// 服务端新增未知 code 时回落原始消息,避免渲染 undefined 或插值崩溃
|
package/lib/client/i18n.js
CHANGED
|
@@ -42,6 +42,7 @@ export const ZH_DICT = {
|
|
|
42
42
|
'common.requestFailed': '请求失败',
|
|
43
43
|
'common.actionFailed': '操作失败',
|
|
44
44
|
'common.requestTimeout': '请求超时,请重试',
|
|
45
|
+
'common.networkError': '网络连接失败,请确认星愿服务已启动后重试',
|
|
45
46
|
'common.dayUnit': '{n} 天',
|
|
46
47
|
'common.countUnit': '{n} 次',
|
|
47
48
|
'common.countUnitOne': '{n} 次',
|
|
@@ -158,9 +159,9 @@ export const ZH_DICT = {
|
|
|
158
159
|
'growth.levelLabel': '当前等级',
|
|
159
160
|
'growth.levelFallback': '初心者',
|
|
160
161
|
'growth.maxed': '已满级',
|
|
161
|
-
'growth.expFormat': '{cur} / {next}
|
|
162
|
+
'growth.expFormat': '{cur} / {next} 经验',
|
|
162
163
|
'growth.levelRequire': '需要 {exp} 经验',
|
|
163
|
-
'growth.rewardPrefix': '
|
|
164
|
+
'growth.rewardPrefix': '当前等级荣誉:{reward}',
|
|
164
165
|
'growth.stat.checkinDays': '累计打卡天数',
|
|
165
166
|
'growth.stat.streak': '连续坚持',
|
|
166
167
|
'growth.stat.maxStreak': '最长连续坚持',
|
|
@@ -181,23 +182,23 @@ export const ZH_DICT = {
|
|
|
181
182
|
'growth.lv.1.name': '初心者',
|
|
182
183
|
'growth.lv.1.reward': '开启星愿之旅',
|
|
183
184
|
'growth.lv.2.name': '探索者',
|
|
184
|
-
'growth.lv.2.reward': '
|
|
185
|
+
'growth.lv.2.reward': '晋升「探索者」称号',
|
|
185
186
|
'growth.lv.3.name': '实践者',
|
|
186
|
-
'growth.lv.3.reward': '
|
|
187
|
+
'growth.lv.3.reward': '晋升「实践者」称号',
|
|
187
188
|
'growth.lv.4.name': '坚持者',
|
|
188
|
-
'growth.lv.4.reward': '
|
|
189
|
+
'growth.lv.4.reward': '晋升「坚持者」称号',
|
|
189
190
|
'growth.lv.5.name': '奋斗者',
|
|
190
|
-
'growth.lv.5.reward': '
|
|
191
|
+
'growth.lv.5.reward': '晋升「奋斗者」称号',
|
|
191
192
|
'growth.lv.6.name': '进取者',
|
|
192
|
-
'growth.lv.6.reward': '
|
|
193
|
+
'growth.lv.6.reward': '晋升「进取者」称号',
|
|
193
194
|
'growth.lv.7.name': '成就者',
|
|
194
|
-
'growth.lv.7.reward': '
|
|
195
|
+
'growth.lv.7.reward': '晋升「成就者」称号',
|
|
195
196
|
'growth.lv.8.name': '卓越者',
|
|
196
|
-
'growth.lv.8.reward': '
|
|
197
|
+
'growth.lv.8.reward': '晋升「卓越者」称号',
|
|
197
198
|
'growth.lv.9.name': '领航者',
|
|
198
|
-
'growth.lv.9.reward': '
|
|
199
|
+
'growth.lv.9.reward': '晋升「领航者」称号',
|
|
199
200
|
'growth.lv.10.name': '星愿大师',
|
|
200
|
-
'growth.lv.10.reward': '
|
|
201
|
+
'growth.lv.10.reward': '晋升「星愿大师」——星愿之旅的最高荣誉',
|
|
201
202
|
// --- 记忆页 ---
|
|
202
203
|
'memory.pageTitle': '记忆',
|
|
203
204
|
'memory.summary': '共 {total} 条 · 对话时按上限自动注入',
|
|
@@ -218,6 +219,7 @@ export const ZH_DICT = {
|
|
|
218
219
|
'memory.footNote': '删除与清空不可恢复;注入条数上限在 设置 → 星愿 调整。',
|
|
219
220
|
'memory.clearAll': '清空全部记忆',
|
|
220
221
|
'memory.needKeyAndValue': '键名和内容都要填写。',
|
|
222
|
+
'memory.keyTooShort': '键名至少需要 2 个字符。',
|
|
221
223
|
'memory.overwriteAsk': '「{key}」已存在,覆盖原内容吗?',
|
|
222
224
|
'memory.confirmDelete': '删除记忆「{key}:{value}」?删除后不可恢复。',
|
|
223
225
|
'memory.confirmClear': '清空全部 {total} 条记忆?此操作不可恢复!',
|
|
@@ -259,7 +261,7 @@ export const ZH_DICT = {
|
|
|
259
261
|
'settings.profile.saved': '画像已保存',
|
|
260
262
|
'settings.profile.sharedHint': '与对话侧共享同一份档案;对话里说「叫我小星」也会更新。',
|
|
261
263
|
'settings.pref.title': '对话偏好',
|
|
262
|
-
'settings.pref.unavailable': '
|
|
264
|
+
'settings.pref.unavailable': '当前连接不支持持久化偏好(远程或临时模式),以下选项不可用。',
|
|
263
265
|
'settings.pref.loading': '偏好加载中…',
|
|
264
266
|
'settings.pref.notRegistered': '偏好暂不可用(组件未就绪),请稍后重试。',
|
|
265
267
|
'settings.pref.readOnly': '当前设置为只读,无法保存修改。',
|
|
@@ -461,6 +463,7 @@ const EN_DICT = {
|
|
|
461
463
|
'common.requestFailed': 'Request failed',
|
|
462
464
|
'common.actionFailed': 'Action failed',
|
|
463
465
|
'common.requestTimeout': 'Request timed out, please retry',
|
|
466
|
+
'common.networkError': 'Network request failed. Make sure the XingYuan service is running, then try again',
|
|
464
467
|
'common.dayUnit': '{n} days',
|
|
465
468
|
'common.countUnit': '{n} times',
|
|
466
469
|
'common.countUnitOne': '{n} time',
|
|
@@ -591,23 +594,23 @@ const EN_DICT = {
|
|
|
591
594
|
'growth.lv.1.name': 'Novice',
|
|
592
595
|
'growth.lv.1.reward': 'Begin your journey',
|
|
593
596
|
'growth.lv.2.name': 'Explorer',
|
|
594
|
-
'growth.lv.2.reward': '
|
|
597
|
+
'growth.lv.2.reward': 'Earn the “Explorer” title',
|
|
595
598
|
'growth.lv.3.name': 'Practitioner',
|
|
596
|
-
'growth.lv.3.reward': 'Earn
|
|
599
|
+
'growth.lv.3.reward': 'Earn the “Practitioner” title',
|
|
597
600
|
'growth.lv.4.name': 'Perseverer',
|
|
598
|
-
'growth.lv.4.reward': '
|
|
601
|
+
'growth.lv.4.reward': 'Earn the “Perseverer” title',
|
|
599
602
|
'growth.lv.5.name': 'Striver',
|
|
600
|
-
'growth.lv.5.reward': 'Earn
|
|
603
|
+
'growth.lv.5.reward': 'Earn the “Striver” title',
|
|
601
604
|
'growth.lv.6.name': 'Go-getter',
|
|
602
|
-
'growth.lv.6.reward': '
|
|
605
|
+
'growth.lv.6.reward': 'Earn the “Go-getter” title',
|
|
603
606
|
'growth.lv.7.name': 'Achiever',
|
|
604
|
-
'growth.lv.7.reward': 'Earn
|
|
607
|
+
'growth.lv.7.reward': 'Earn the “Achiever” title',
|
|
605
608
|
'growth.lv.8.name': 'Paragon',
|
|
606
|
-
'growth.lv.8.reward': '
|
|
609
|
+
'growth.lv.8.reward': 'Earn the “Paragon” title',
|
|
607
610
|
'growth.lv.9.name': 'Pathfinder',
|
|
608
|
-
'growth.lv.9.reward': 'Earn
|
|
611
|
+
'growth.lv.9.reward': 'Earn the “Pathfinder” title',
|
|
609
612
|
'growth.lv.10.name': 'XingYuan Master',
|
|
610
|
-
'growth.lv.10.reward': '
|
|
613
|
+
'growth.lv.10.reward': 'Earn the “XingYuan Master” title — the highest honor of the journey',
|
|
611
614
|
'memory.pageTitle': 'Memories',
|
|
612
615
|
'memory.summary': '{total} in total · auto-injected up to the limit during chats',
|
|
613
616
|
'memory.editing': 'Editing “{key}” (key is fixed)',
|
|
@@ -627,9 +630,10 @@ const EN_DICT = {
|
|
|
627
630
|
'memory.footNote': 'Deletions are permanent; adjust the injection limit under Settings → XingYuan.',
|
|
628
631
|
'memory.clearAll': 'Clear all memories',
|
|
629
632
|
'memory.needKeyAndValue': 'Both key and content are required.',
|
|
633
|
+
'memory.keyTooShort': 'Key must be at least 2 characters.',
|
|
630
634
|
'memory.overwriteAsk': '“{key}” already exists. Overwrite it?',
|
|
631
635
|
'memory.confirmDelete': 'Delete memory “{key}: {value}”? This cannot be undone.',
|
|
632
|
-
'memory.confirmClear': 'Clear all {total}
|
|
636
|
+
'memory.confirmClear': 'Clear all memories ({total})? This cannot be undone!',
|
|
633
637
|
'memory.savedNew': 'Added “{key}”',
|
|
634
638
|
'memory.savedOverwrite': 'Saved “{key}”',
|
|
635
639
|
'memory.deletedOne': '“{key}” deleted',
|
|
@@ -667,7 +671,7 @@ const EN_DICT = {
|
|
|
667
671
|
'settings.profile.saved': 'Profile saved',
|
|
668
672
|
'settings.profile.sharedHint': 'Shared with the chat side; saying “call me Star” in chat updates it too.',
|
|
669
673
|
'settings.pref.title': 'Chat preferences',
|
|
670
|
-
'settings.pref.unavailable': 'This connection cannot persist preferences (remote or temporary mode); the
|
|
674
|
+
'settings.pref.unavailable': 'This connection cannot persist preferences (remote or temporary mode); the options below are disabled.',
|
|
671
675
|
'settings.pref.loading': 'Loading preferences…',
|
|
672
676
|
'settings.pref.notRegistered': 'Preferences are unavailable right now (component not ready); please retry later.',
|
|
673
677
|
'settings.pref.readOnly': 'Settings are read-only; changes cannot be saved.',
|
|
@@ -749,7 +753,7 @@ const EN_DICT = {
|
|
|
749
753
|
'chart.subtitle.daysRatio': '{done}/{due} days',
|
|
750
754
|
'chart.subtitle.streak': 'Current {current}d / best {best}d',
|
|
751
755
|
'detail.grid.title': 'Check-in record',
|
|
752
|
-
'detail.grid.summary': 'Last {total}
|
|
756
|
+
'detail.grid.summary': 'Last {total} cell(s): {checked} checked, {missed} missed, {future} pending',
|
|
753
757
|
'detail.legend.checked': 'Checked in',
|
|
754
758
|
'detail.legend.missed': 'Missed',
|
|
755
759
|
'detail.legend.future': 'Pending today',
|
|
@@ -905,7 +909,12 @@ export function useXyT() {
|
|
|
905
909
|
return xyTranslateStable;
|
|
906
910
|
}
|
|
907
911
|
const xyTranslateStable = (key, params) => t(key, params);
|
|
908
|
-
/** 当前语言(供调试/埋点类场景;无 face 时返回 'zh' 兜底口径)。
|
|
912
|
+
/** 当前语言(供调试/埋点类场景;无 face 时返回 'zh' 兜底口径)。
|
|
913
|
+
* 平台事实(rc.2 实测):宿主发布的 LocaleFace 契约(dsh-client-ui-slots
|
|
914
|
+
* renderer.d.ts)只声明 getSnapshot(): { revision } 与 bind(ns),快照的 `active`
|
|
915
|
+
* 字段不在类型面内但运行时存在——本函数依赖它做语言判定,读取处已做 unknown
|
|
916
|
+
* 收窄,字段缺席时静默回落 zh(en 用户会看到英文正文配中文日期,而非崩溃)。
|
|
917
|
+
* 上游若把 active 纳入正式契约,此处无需改动。 */
|
|
909
918
|
export function activeLocale() {
|
|
910
919
|
if (localeFace === undefined)
|
|
911
920
|
return 'zh';
|
|
@@ -32,8 +32,9 @@ function levelRewardKey(level) {
|
|
|
32
32
|
}
|
|
33
33
|
function localLevelName(level, fallback) {
|
|
34
34
|
const dict = translate(levelNameKey(level));
|
|
35
|
-
// 键缺词时 t() 回落键名本身——zh 词表覆盖 1-10
|
|
36
|
-
|
|
35
|
+
// 键缺词时 t() 回落键名本身——zh 词表覆盖 1-10,命中即用;否则用服务端原文;
|
|
36
|
+
// 两者皆缺(未知等级且服务端未带名)落到词典兜底键,读屏不再得到空等级名
|
|
37
|
+
return dict !== levelNameKey(level) ? dict : (fallback ?? translate('growth.levelFallback'));
|
|
37
38
|
}
|
|
38
39
|
function localLevelReward(level, fallback) {
|
|
39
40
|
const dict = translate(levelRewardKey(level));
|
|
@@ -178,6 +178,12 @@ export function MemoryPage() {
|
|
|
178
178
|
setFormError(t('memory.needKeyAndValue'));
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
|
+
// 与服务端 schema 同口径(min 2):在错误发生的位置就地引导,而非让服务端
|
|
182
|
+
// 的 missing_field 被客户端按 code 覆盖成通用文案
|
|
183
|
+
if (key.length < 2) {
|
|
184
|
+
setFormError(t('memory.keyTooShort'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
181
187
|
setFormError(undefined);
|
|
182
188
|
const knownExists = editingKey !== undefined || allItems.some((m) => m.key === key);
|
|
183
189
|
if (knownExists && editingKey === undefined) {
|
|
@@ -260,10 +266,18 @@ export function MemoryPage() {
|
|
|
260
266
|
value: query, onChange: (e) => setQuery(e.target.value),
|
|
261
267
|
}), notice !== undefined ? createElement('span', { className: 'xy-saved', role: 'status' }, notice) : null), data.total > allItems.length
|
|
262
268
|
? createElement('div', { className: 'xy-meta xy-memcap' }, t('memory.capNote', { total: data.total, shown: allItems.length }))
|
|
263
|
-
: null, data.total === 0
|
|
264
|
-
?
|
|
269
|
+
: null, data.total === 0 && debounced.trim() === ''
|
|
270
|
+
? // 真·空库。服务端 total 本身按关键词过滤,settle 后零命中搜索的 total 也是
|
|
271
|
+
// 0——不以「无搜索词」为前提区分的话,零命中会错用「还没有任何记忆」文案
|
|
272
|
+
createElement(PageEmpty, { title: t('memory.empty.title'), hint: t('memory.empty.hint') })
|
|
265
273
|
: filtered.length === 0
|
|
266
|
-
?
|
|
274
|
+
? // 空态仅在防抖 settle 后判定:本地过滤用逐键 query、数据按防抖词取回,
|
|
275
|
+
// 输入中渲染空白(null = 卸载该节点,无行可显示时不闪空态文案)。
|
|
276
|
+
// 已知残余:debounce settle → 新数据落地之间旧数据按新词过滤为空时,
|
|
277
|
+
// 仍可能短暂显示一次空态(数据载荷不带检索词,无法精确判定,量级可接受)
|
|
278
|
+
query.trim() === debounced.trim()
|
|
279
|
+
? createElement(PageEmpty, { title: t('memory.searchEmpty.title') })
|
|
280
|
+
: null
|
|
267
281
|
: // 列表收进单张分组卡:分隔线行替代逐行描边盒子,长列表不再满屏边框噪音
|
|
268
282
|
createElement('section', { className: 'xy-group' }, createElement('ul', { className: 'xy-grouplist' }, rows)), hasMore
|
|
269
283
|
? createElement('div', { className: 'xy-memfoot' }, createElement('button', { className: 'xy-btn', disabled: loadingMore || busy, onClick: loadMore }, loadingMore ? `${t('common.loading')}…` : t('memory.more')), loadError !== undefined ? createElement('span', { className: 'xy-field-err' }, loadError) : null)
|
|
@@ -1,25 +1,40 @@
|
|
|
1
1
|
/** 任务行(愿望页/任务页共用):状态元信息 + 领取/打卡内联动作。 */
|
|
2
2
|
import { createElement } from 'react';
|
|
3
3
|
import { postAction } from '../api.js';
|
|
4
|
-
import { toast } from '../ui.js';
|
|
4
|
+
import { focusPageTitle, toast } from '../ui.js';
|
|
5
5
|
import { useXyT } from '../i18n.js';
|
|
6
6
|
import { softConfirm, useActionGuard } from '../hooks.js';
|
|
7
7
|
import { cycleLabel, durationText, closedStatusLabel, dateSuffix, formatShortDate } from './format.js';
|
|
8
8
|
export function TaskLine(props) {
|
|
9
9
|
const t = useXyT();
|
|
10
|
-
const { task, today, onChanged, trailing } = props;
|
|
10
|
+
const { task, today, onChanged, trailing, focusAfterClaim } = props;
|
|
11
11
|
const { busy, guard } = useActionGuard();
|
|
12
12
|
const act = (action, body, doneText) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
// 返回动作 promise(闭包捕获,与今日页同款)供调用方接续焦点移交
|
|
14
|
+
let run;
|
|
15
|
+
guard(() => {
|
|
16
|
+
run = postAction(action, body).then((payload) => {
|
|
17
|
+
if (doneText !== undefined)
|
|
18
|
+
toast(doneText(payload), 'ok');
|
|
19
|
+
return Promise.resolve(onChanged()).then(() => undefined);
|
|
20
|
+
});
|
|
21
|
+
return run;
|
|
22
|
+
});
|
|
23
|
+
return run ?? Promise.resolve();
|
|
18
24
|
};
|
|
19
25
|
// 未来机会日:预勾 = 承诺当天完成(与对话侧确认语义一致),按钮如实标注并二次确认
|
|
20
26
|
const futureDate = task.nextOpportunityDate !== undefined && task.nextOpportunityDate > today ? task.nextOpportunityDate : undefined;
|
|
21
27
|
const checkIn = () => {
|
|
22
|
-
const submit = () =>
|
|
28
|
+
const submit = () => {
|
|
29
|
+
// 一次打卡使任务完结时(任务页跨状态分组 / 愿望卡行迁入已完成),原按钮随
|
|
30
|
+
// DOM 销毁、焦点落空——交给页面标题兜底;未完结的行原地复用 DOM,无需移动焦点
|
|
31
|
+
// 镜像 isTaskDone 口径:requiredDays=0(无截止日不限次)任务永不关闭,
|
|
32
|
+
// 打卡后行原地复用 DOM,不该把焦点拽离
|
|
33
|
+
const closes = task.requiredDays > 0 && task.completedDays + 1 >= task.requiredDays;
|
|
34
|
+
void act('checkin', { taskId: task.taskId }, (p) => t('toast.checkinOk') + dateSuffix(typeof p.date === 'string' ? String(p.date) : undefined))
|
|
35
|
+
.then(() => { if (closes)
|
|
36
|
+
focusPageTitle(); }, () => { });
|
|
37
|
+
};
|
|
23
38
|
if (futureDate === undefined) {
|
|
24
39
|
submit();
|
|
25
40
|
return;
|
|
@@ -35,7 +50,11 @@ export function TaskLine(props) {
|
|
|
35
50
|
+ `${task.dueDate !== undefined ? ` · ${t('task.due', { date: formatShortDate(task.dueDate) })}` : ''}`), createElement('div', { style: { display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' } }, task.status === 'pending'
|
|
36
51
|
? createElement('button', {
|
|
37
52
|
className: 'xy-btn', disabled: busy,
|
|
38
|
-
onClick: () =>
|
|
53
|
+
onClick: () => {
|
|
54
|
+
void act('claim', { taskId: task.taskId }, () => t('toast.claimed', { name: task.name }))
|
|
55
|
+
.then(() => { if (focusAfterClaim)
|
|
56
|
+
focusPageTitle(); }, () => { });
|
|
57
|
+
},
|
|
39
58
|
}, t('action.claim'))
|
|
40
59
|
: task.status === 'in_progress'
|
|
41
60
|
? createElement('button', { className: 'xy-btn xy-btn-primary', disabled: busy, onClick: checkIn }, futureDate !== undefined ? t('action.checkinFuture', { date: formatShortDate(futureDate) }) : t('action.checkin'))
|
|
@@ -46,6 +46,8 @@ export function TasksPage() {
|
|
|
46
46
|
task,
|
|
47
47
|
today: data.today,
|
|
48
48
|
onChanged: page.reload,
|
|
49
|
+
// 任务页按状态分桶:领取使行迁移组别,领取后焦点交页面标题兜底
|
|
50
|
+
focusAfterClaim: true,
|
|
49
51
|
trailing: createElement(DetailToggle, {
|
|
50
52
|
open: expanded.has(task.taskId),
|
|
51
53
|
onToggle: () => toggleDetail(task.taskId),
|
|
@@ -56,7 +56,12 @@ export function TodayPage() {
|
|
|
56
56
|
// planForDay 保证展示行必为未打卡的进行中任务,canCheckIn 恒 true——
|
|
57
57
|
// 不再有「disabled 但无解释」的死分支;disabled 仅服务动作在途的 busy 窗口
|
|
58
58
|
disabled: busy || !task.canCheckIn,
|
|
59
|
-
onClick: () =>
|
|
59
|
+
onClick: () => {
|
|
60
|
+
void act('checkin', { taskId: task.taskId }, (p) => t('toast.checkinOk') + dateSuffix(typeof p.date === 'string' ? String(p.date) : undefined))
|
|
61
|
+
// 行随刷新从「待完成」组迁入「已完成」组、原按钮销毁,焦点交给页面
|
|
62
|
+
// 标题兜底(与下方撤销路径同款;领取行留在同一列表内不迁移,无需兜底)
|
|
63
|
+
.then(focusPageTitle, () => { });
|
|
64
|
+
},
|
|
60
65
|
}, t('action.checkin'))
|
|
61
66
|
: createElement('button', {
|
|
62
67
|
className: 'xy-btn', disabled: busy,
|
package/lib/client/styles.js
CHANGED
|
@@ -56,6 +56,10 @@ export const STYLE_TEXT = `
|
|
|
56
56
|
--xyd-c3-soft-dot:rgba(16,185,129,.30);
|
|
57
57
|
--xyd-dcell-checked:rgba(56,217,169,.42);
|
|
58
58
|
--xyd-dcell-checked-dot:rgba(56,217,169,.55);
|
|
59
|
+
/* 上述成对底色的前景伙伴(同使用点收进令牌区,漂移即红) */
|
|
60
|
+
--xyd-on-c2:#7c4a03;
|
|
61
|
+
--xyd-on-c3:#065f46;
|
|
62
|
+
--xyd-on-dcell:#0b3d26;
|
|
59
63
|
/* 阴影对(卡片悬停/toast/弹窗/英雄卡):深浅各自配档,禁止正文区裸写 rgba */
|
|
60
64
|
--xyd-shadow-card:0 2px 10px rgba(15,23,42,.07);
|
|
61
65
|
--xyd-shadow-toast:0 8px 24px -8px rgba(15,23,42,.2),0 2px 8px -2px rgba(15,23,42,.1);
|
|
@@ -102,6 +106,9 @@ body[data-ds-dark-theme]{
|
|
|
102
106
|
--xyd-c3-soft-dot:rgba(52,211,153,.30);
|
|
103
107
|
--xyd-dcell-checked:rgba(46,160,105,.32);
|
|
104
108
|
--xyd-dcell-checked-dot:rgba(46,160,105,.45);
|
|
109
|
+
--xyd-on-c2:#ffd9a8;
|
|
110
|
+
--xyd-on-c3:#a7f3c9;
|
|
111
|
+
--xyd-on-dcell:#a7f3c9;
|
|
105
112
|
--xyd-shadow-card:0 2px 10px rgba(0,0,0,.32);
|
|
106
113
|
--xyd-shadow-toast:0 8px 24px -8px rgba(0,0,0,.5),0 2px 8px -2px rgba(0,0,0,.35);
|
|
107
114
|
--xyd-shadow-modal:0 24px 48px -16px rgba(0,0,0,.55),0 4px 12px -4px rgba(0,0,0,.4);
|
|
@@ -129,7 +136,7 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
129
136
|
.xy-badge-chart{background:var(--xyd-ok-badge)}
|
|
130
137
|
.xy-badge-micro{background:var(--xyd-warn-soft)}
|
|
131
138
|
.xy-title{min-width:0;font-weight:600;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
|
|
132
|
-
.xy-deleted{opacity:.
|
|
139
|
+
.xy-deleted{opacity:.65}
|
|
133
140
|
.xy-deleted .xy-title{text-decoration:line-through}
|
|
134
141
|
.xy-meta{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:4px;overflow-wrap:anywhere}
|
|
135
142
|
/* 次级面板底上的次要文字用专用对(壳通用次要色对 layer-2 仅 ≈4.3:1,双主题同病) */
|
|
@@ -146,9 +153,12 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
146
153
|
.xy-microstep{display:flex;align-items:flex-start;gap:8px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--xyd-r-inner);background:var(--dsw-alias-bg-layer-2)}
|
|
147
154
|
.xy-microstep.xy-done{opacity:.74}
|
|
148
155
|
.xy-microstep.xy-done .xy-microsteptext{text-decoration:line-through}
|
|
149
|
-
.xy-microstep.xy-skipped{opacity:.
|
|
156
|
+
.xy-microstep.xy-skipped{opacity:.65}
|
|
150
157
|
.xy-microstepnum{flex:none;width:20px;height:20px;border-radius:50%;background:var(--xyd-accent);color:var(--xyd-on-accent);display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;margin-top:1px}
|
|
151
158
|
.xy-microsteptext{font-size:13px;display:flex;flex-direction:column;min-width:0}
|
|
159
|
+
/* 卡内步骤说明/状态词的背景是 layer-2:次要文字用 label-on-2 专用对(壳通用
|
|
160
|
+
* 次要色对 layer-2 仅 ≈4.3:1,双主题同病——与 xy-preview 同一治理线) */
|
|
161
|
+
.xy-microstep .xy-meta{color:var(--xyd-label-on-2)}
|
|
152
162
|
.xy-microstate{margin-left:auto;flex:none}
|
|
153
163
|
|
|
154
164
|
/* 图表卡 */
|
|
@@ -356,14 +366,15 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
356
366
|
* (c1 底色对卡底仅 ≈1.1:1,图例点有描边而圆章没有——两侧观感不一致) */
|
|
357
367
|
.xy-c1 .xy-daynum{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2)}
|
|
358
368
|
/* 部分完成 / 全部完成 = 柔和语义底,字色按主题配对保证 ≥4.5:1 */
|
|
359
|
-
.xy-c2 .xy-daynum{background:var(--xyd-c2-soft);color
|
|
360
|
-
.xy-c3 .xy-daynum{background:var(--xyd-c3-soft);color
|
|
361
|
-
body[data-ds-dark-theme] .xy-c2 .xy-daynum{color
|
|
362
|
-
body[data-ds-dark-theme] .xy-c3 .xy-daynum{color
|
|
363
|
-
/* hover
|
|
364
|
-
*
|
|
369
|
+
.xy-c2 .xy-daynum{background:var(--xyd-c2-soft);color:var(--xyd-on-c2)}
|
|
370
|
+
.xy-c3 .xy-daynum{background:var(--xyd-c3-soft);color:var(--xyd-on-c3)}
|
|
371
|
+
body[data-ds-dark-theme] .xy-c2 .xy-daynum{color:var(--xyd-on-c2)}
|
|
372
|
+
body[data-ds-dark-theme] .xy-c3 .xy-daynum{color:var(--xyd-on-c3)}
|
|
373
|
+
/* hover 晕底排除已选中格与语义状态格:specificity 高于单类选择器,不排除的话
|
|
374
|
+
* 悬停在已选日上会用晕底盖掉选中实底(视觉上「取消选中」);c2/c3 同理——
|
|
375
|
+
* 状态底色正是用户悬停核对的那个信息,不能在悬停瞬间被晕底抹掉 */
|
|
365
376
|
@media (hover:hover) and (pointer:fine){
|
|
366
|
-
.xy-cell:not(.xy-outside):not(.xy-picked):hover .xy-daynum{background:var(--xyd-accent-soft)}
|
|
377
|
+
.xy-cell:not(.xy-outside):not(.xy-picked):not(.xy-c2):not(.xy-c3):hover .xy-daynum{background:var(--xyd-accent-soft)}
|
|
367
378
|
}
|
|
368
379
|
/* 今日 = accent 环包圆章:不覆盖状态底色(「今天该打卡」的提示不能被吃掉) */
|
|
369
380
|
.xy-today .xy-daynum{box-shadow:0 0 0 2px var(--xyd-accent)}
|
|
@@ -450,7 +461,7 @@ button.xy-growth-col.xy-hover{box-shadow:inset 0 0 0 2px var(--xyd-accent-ring)}
|
|
|
450
461
|
.xy-compose select.xy-input{width:auto}
|
|
451
462
|
.xy-editing{display:flex;align-items:center;gap:10px;margin-top:12px;padding:7px 10px;border:1px dashed var(--dsw-alias-border-l2);border-radius:var(--xyd-r-inner)}
|
|
452
463
|
.xy-memactions{display:flex;gap:6px;flex:none}
|
|
453
|
-
.xy-membar{display:flex;align-items:center;gap:10px;margin-top:14px}
|
|
464
|
+
.xy-membar{display:flex;align-items:center;gap:10px;margin-top:14px;flex-wrap:wrap}
|
|
454
465
|
.xy-membar .xy-input-search{width:min(250px,100%)}
|
|
455
466
|
.xy-memcap{margin-top:6px}
|
|
456
467
|
/* 超长记忆值 3 行截断(≤1000 字符记录不再撑出数屏高行;完整值在删除确认弹窗可见) */
|
|
@@ -495,8 +506,8 @@ button.xy-growth-col.xy-hover{box-shadow:inset 0 0 0 2px var(--xyd-accent-ring)}
|
|
|
495
506
|
.xy-dcell-blank{visibility:hidden}
|
|
496
507
|
/* 今日格 = accent 环包底色:与日历页圆章的 today 环同一语法 */
|
|
497
508
|
.xy-dcell.xy-dcell-today{box-shadow:0 0 0 2px var(--xyd-accent)}
|
|
498
|
-
.xy-dcell-checked{background:var(--xyd-dcell-checked);color
|
|
499
|
-
body[data-ds-dark-theme] .xy-dcell-checked{color
|
|
509
|
+
.xy-dcell-checked{background:var(--xyd-dcell-checked);color:var(--xyd-on-dcell)}
|
|
510
|
+
body[data-ds-dark-theme] .xy-dcell-checked{color:var(--xyd-on-dcell)}
|
|
500
511
|
.xy-dcell-missed{background:transparent;border:1px dashed var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary)}
|
|
501
512
|
.xy-dcell-future{background:var(--dsw-alias-bg-layer-2);color:var(--xyd-label-on-2);border:1px solid var(--dsw-alias-border-l1)}
|
|
502
513
|
/* 图例复用 xy-dot 时借格子的语义底色(同色系小尺寸) */
|