@starwish-ai/xingyuan-dsh 0.6.0 → 0.6.2
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 +46 -0
- package/README.en-US.md +2 -2
- package/README.md +2 -2
- package/lib/client/i18n.js +26 -4
- package/lib/client/pages/settings.js +67 -3
- package/lib/client/pages/wishes.js +32 -5
- package/lib/client/styles.js +30 -5
- package/lib/client/ui.js +7 -0
- package/lib/client.js +203 -16
- package/lib/index.js +5 -2
- package/lib/pref-policy.js +37 -3
- package/lib/pref-settings.js +11 -1
- package/lib/preset/prompts.js +3 -3
- package/lib/preset/side.js +1 -0
- package/lib/preset/tools.js +70 -16
- package/lib/sqlite.js +11 -3
- package/lib/types/client/i18n.d.ts +14 -3
- package/lib/types/client/pages/settings.d.ts +1 -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/index.d.ts +3 -1
- package/lib/types/pref-policy.d.ts +24 -4
- package/lib/types/preset/hitl.d.ts +4 -3
- package/lib/types/preset/tools.d.ts +6 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ 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.2] - 2026-09-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Fine-grained write-confirmation settings**: the master switch ("写操作二次确认")
|
|
12
|
+
now expands into six per-category detail toggles — create (incl. micro-action
|
|
13
|
+
breakdown), check-in, cancel check-in, claim, edit, memory save. Detail rows stay
|
|
14
|
+
visible under the master switch and grey out (explicit disabled styling) when it is
|
|
15
|
+
off; the delete row remains locked (always confirmed, no off switch). Defaults
|
|
16
|
+
reproduce the previous matrix: create / check-in / cancel check-in ask; claim /
|
|
17
|
+
edit / memory save run on your instruction. Claim, edit and memory-save tools
|
|
18
|
+
gained real confirmation hooks (600 s HITL timeout, validation before asking,
|
|
19
|
+
cancel exits without writing). Chat-side HITL only — page dialogs are unchanged.
|
|
20
|
+
Zero migration: absent settings resolve to the defaults.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `rename_wish_category` validates before asking: renaming a non-existent category
|
|
24
|
+
errors up front instead of after a confirmation; renaming a color-override-only
|
|
25
|
+
category (zero wishes) now succeeds and reports "0 wishes" instead of falsely
|
|
26
|
+
erroring "category does not exist" after the override had already been migrated.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- sqlite backend `close()` now actually releases the database handle (was an empty
|
|
30
|
+
implementation), so hot-reload / unload no longer keeps the file locked.
|
|
31
|
+
- Bundle main row declares the previously missing
|
|
32
|
+
`storageBackendServiceKey('sqlite')` injection key.
|
|
33
|
+
- StaleBanner text contrast: new `--xyd-on-warn` token pair reaches WCAG 4.5:1 on the
|
|
34
|
+
warn background in both light and dark themes (locked by a WCAG computation test).
|
|
35
|
+
- README confirmation wording no longer contradicts AGENTS.md §5.5.
|
|
36
|
+
|
|
37
|
+
## [0.6.1] - 2026-09-05
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- **Wishes page: collapsible task area**: each wish card's task list now starts collapsed
|
|
41
|
+
behind a full-row disclosure toggle ("N 个任务" / "Tasks (N)" with a chevron that flips
|
|
42
|
+
when expanded). Card-header progress, the "N to claim" badge and the wrap-up guidance
|
|
43
|
+
line stay visible while collapsed; expanding restores the task rows exactly as before —
|
|
44
|
+
claim / check-in buttons and the inline per-task detail expansion now nest inside the
|
|
45
|
+
card-level disclosure. Achieved wishes collapse the same way; wishes without tasks keep
|
|
46
|
+
their existing hint and get no toggle. Expanded cards are remembered across view-tab
|
|
47
|
+
switches (view-state snapshot). Client-only: no API, storage or write-path changes.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Wish-card copy de-jargoned: the empty hint now says "还没有任务" (was "暂无下属任务")
|
|
51
|
+
and the delete-wish confirmation says "它的任务…" (was "下属任务…"), matching the
|
|
52
|
+
established plain-wording standard for user-facing surfaces.
|
|
53
|
+
|
|
8
54
|
## [0.6.0] - 2026-09-05
|
|
9
55
|
|
|
10
56
|
> **Note:** behavioral revision — long-term ratios (wish progress, progress charts) join
|
package/README.en-US.md
CHANGED
|
@@ -18,7 +18,7 @@ XingYuan is a wish & habit-building companion, shipped as a DeepSeek Harness plu
|
|
|
18
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
|
|
19
19
|
- **Memory**: important memories injected into context (limit configurable), full CRUD
|
|
20
20
|
- **Charts**: 15 chart types rendered as cards
|
|
21
|
-
- **Safe writes**: write
|
|
21
|
+
- **Safe writes**: write confirmation is per-category (create / check-in / undo / claim / edit / save memory); deletion always confirms and cannot be turned off; defaults match previous versions
|
|
22
22
|
- **Theming**: light/dark follows the app theme
|
|
23
23
|
|
|
24
24
|
Tools are only mounted on sessions using the XingYuan preset; other sessions are unaffected.
|
|
@@ -41,7 +41,7 @@ Business data lives at `~/.dsh/xingyuan/xingyuan.sqlite` and survives uninstall
|
|
|
41
41
|
| --- | --- |
|
|
42
42
|
| Coach style | Gentle / humorous / strict |
|
|
43
43
|
| User profile | Nickname, occupation, interests |
|
|
44
|
-
| Write confirmation |
|
|
44
|
+
| Write confirmation | Master switch + per-category toggles (create / check-in / undo / claim / edit / save memory); deletion always confirms and cannot be turned off |
|
|
45
45
|
| Memory injection limit | Max memories injected per turn |
|
|
46
46
|
| Tab visibility | Three modes (follow session / always show / always hide) plus per-tab toggles; defaults to follow session |
|
|
47
47
|
| Confirm card language | Language of the in-chat confirmation card (Chinese / English; the platform does not expose UI language to plugins, defaults to Chinese) |
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
默认仅星愿预设的会话显示,可在设置里切换「始终显示 / 始终隐藏」并按标签勾选
|
|
21
21
|
- **记忆**:重要记忆自动注入上下文(上限可配),支持增删改查
|
|
22
22
|
- **图表**:15 种统计图表自动渲染为卡片
|
|
23
|
-
-
|
|
23
|
+
- **安全确认**:写操作二次确认按类目细化(创建 / 打卡 / 取消打卡 / 领取 / 修改 / 保存记忆),删除始终确认不可关闭;默认与此前版本一致
|
|
24
24
|
- **主题适配**:深浅色跟随应用主题
|
|
25
25
|
|
|
26
26
|
工具只挂载在选择星愿 preset 的会话上,不影响其他会话。
|
|
@@ -43,7 +43,7 @@ dsh plugin --profile web add @starwish-ai/xingyuan-dsh
|
|
|
43
43
|
| --- | --- |
|
|
44
44
|
| 教练风格 | 温柔 / 幽默 / 严格 |
|
|
45
45
|
| 用户画像 | 昵称、职业、兴趣 |
|
|
46
|
-
| 写操作二次确认 |
|
|
46
|
+
| 写操作二次确认 | 总开关 + 六个类目明细(创建 / 打卡 / 取消打卡 / 领取 / 修改 / 保存记忆)是否确认;删除始终确认、不可关闭 |
|
|
47
47
|
| 记忆注入上限 | 每次注入上下文的记忆条数上限 |
|
|
48
48
|
| 标签页显示 | 三态模式(跟随会话 / 始终显示 / 始终隐藏)+ 六个标签逐个勾选;默认跟随会话 |
|
|
49
49
|
| 确认卡语言 | 对话中确认卡的显示语言(中文 / English;平台不向插件暴露界面语言,默认中文) |
|
package/lib/client/i18n.js
CHANGED
|
@@ -64,7 +64,8 @@ export const ZH_DICT = {
|
|
|
64
64
|
'wish.settled': '待收尾',
|
|
65
65
|
'wish.settleHint': '答应自己的都做完了——待领取的领了继续,或删掉就达成',
|
|
66
66
|
'wish.eta': '预计完成 {date}',
|
|
67
|
-
'wish.noTasks': '
|
|
67
|
+
'wish.noTasks': '还没有任务——在对话里告诉我,让我为它推荐打卡任务',
|
|
68
|
+
'wish.tasksToggle': '{n} 个任务',
|
|
68
69
|
'wish.pageTitle': '我的愿望',
|
|
69
70
|
'wish.summary': '{active} 个进行中{planning}{settled}{achieved}',
|
|
70
71
|
'wish.planningSuffix': ' · {n} 个计划中',
|
|
@@ -128,7 +129,7 @@ export const ZH_DICT = {
|
|
|
128
129
|
'confirm.undoAt': '确定撤销「{name}」在{date}的打卡吗?进度将回退。',
|
|
129
130
|
'confirm.futureAt': '「{name}」的打卡日 {date} 在今天之后,提前打卡表示承诺当天完成。确认?',
|
|
130
131
|
'confirm.deleteTask': '确定删除任务「{name}」吗?其打卡记录与微行动拆解将一并删除,不可恢复。',
|
|
131
|
-
'confirm.deleteWish': '确定删除愿望「{name}
|
|
132
|
+
'confirm.deleteWish': '确定删除愿望「{name}」吗?它的任务、打卡记录与微行动拆解将一并删除,不可恢复。',
|
|
132
133
|
'confirm.renameCategory': '把分类「{old}」下的全部愿望改名为「{new}」吗?(共 {count} 个)', 'confirm.deleteEmptyCategory': '删除空分类「{name}」的自定义颜色吗?',
|
|
133
134
|
// --- 今日页 ---
|
|
134
135
|
'today.title': '今日打卡 · {date}',
|
|
@@ -274,7 +275,17 @@ export const ZH_DICT = {
|
|
|
274
275
|
'settings.pref.notRegistered': '偏好暂不可用(组件未就绪),请稍后重试。',
|
|
275
276
|
'settings.pref.readOnly': '当前设置为只读,无法保存修改。',
|
|
276
277
|
'settings.pref.confirmWrites': '写操作二次确认',
|
|
277
|
-
'settings.pref.confirmWritesHint': '
|
|
278
|
+
'settings.pref.confirmWritesHint': '总开关:开启后按下方类目决定是否弹确认卡,关闭后除删除(始终确认)外一律直接执行。',
|
|
279
|
+
'settings.pref.confirmOps.group': '确认类目',
|
|
280
|
+
'settings.pref.confirmOps.create': '创建愿望/任务(含微行动拆解)',
|
|
281
|
+
'settings.pref.confirmOps.checkin': '打卡',
|
|
282
|
+
'settings.pref.confirmOps.cancelCheckin': '取消打卡',
|
|
283
|
+
'settings.pref.confirmOps.claim': '领取任务',
|
|
284
|
+
'settings.pref.confirmOps.update': '修改愿望/任务/分类',
|
|
285
|
+
'settings.pref.confirmOps.memorySave': '保存记忆',
|
|
286
|
+
'settings.pref.confirmOps.delete': '删除(含批量、微行动重开)',
|
|
287
|
+
'settings.pref.confirmOps.deleteHint': '始终确认,不可关闭:删除不可恢复。',
|
|
288
|
+
'settings.pref.confirmOps.hint': '类目明细在总开关开启时生效;默认与之前版本一致。',
|
|
278
289
|
'settings.pref.memoryLimit': '记忆注入上限',
|
|
279
290
|
'settings.pref.memoryLimitHint': '每次对话自动注入上下文的记忆条数上限(5-200,默认 40);失焦后保存。',
|
|
280
291
|
'settings.pref.limitInvalid': '请输入 5-200 的整数',
|
|
@@ -493,6 +504,7 @@ const EN_DICT = {
|
|
|
493
504
|
'wish.settleHint': 'Everything you committed to is done — claim the rest to keep going, or delete them to achieve the wish',
|
|
494
505
|
'wish.eta': 'Target date {date}',
|
|
495
506
|
'wish.noTasks': 'No tasks yet — tell me in chat and I will suggest check-in tasks for it',
|
|
507
|
+
'wish.tasksToggle': 'Tasks ({n})',
|
|
496
508
|
'wish.pageTitle': 'My Wishes',
|
|
497
509
|
'wish.summary': '{active} in progress{planning}{settled}{achieved}',
|
|
498
510
|
'wish.planningSuffix': ' · {n} planning',
|
|
@@ -692,7 +704,17 @@ const EN_DICT = {
|
|
|
692
704
|
'settings.pref.notRegistered': 'Preferences are unavailable right now (component not ready); please retry later.',
|
|
693
705
|
'settings.pref.readOnly': 'Settings are read-only; changes cannot be saved.',
|
|
694
706
|
'settings.pref.confirmWrites': 'Confirm write actions',
|
|
695
|
-
'settings.pref.confirmWritesHint': '
|
|
707
|
+
'settings.pref.confirmWritesHint': 'Master switch: when on, confirmations follow the categories below; when off, everything except deletes (always confirmed) runs directly.',
|
|
708
|
+
'settings.pref.confirmOps.group': 'Confirmation categories',
|
|
709
|
+
'settings.pref.confirmOps.create': 'Create wishes/tasks (incl. micro-action plans)',
|
|
710
|
+
'settings.pref.confirmOps.checkin': 'Check-ins',
|
|
711
|
+
'settings.pref.confirmOps.cancelCheckin': 'Cancel check-ins',
|
|
712
|
+
'settings.pref.confirmOps.claim': 'Claiming tasks',
|
|
713
|
+
'settings.pref.confirmOps.update': 'Editing wishes/tasks/categories',
|
|
714
|
+
'settings.pref.confirmOps.memorySave': 'Saving memories',
|
|
715
|
+
'settings.pref.confirmOps.delete': 'Deletes (incl. batch & restarts)',
|
|
716
|
+
'settings.pref.confirmOps.deleteHint': 'Always confirms and cannot be turned off; deletes are irreversible.',
|
|
717
|
+
'settings.pref.confirmOps.hint': 'Category switches apply while the master switch is on; defaults match previous versions.',
|
|
696
718
|
'settings.pref.memoryLimit': 'Memory injection limit',
|
|
697
719
|
'settings.pref.memoryLimitHint': 'Max memories auto-injected per chat (5-200, default 40); saved when you leave the field.',
|
|
698
720
|
'settings.pref.limitInvalid': 'Enter an integer between 5 and 200',
|
|
@@ -6,7 +6,7 @@ import { getJson, postJson } from '../api.js';
|
|
|
6
6
|
import { toastError } from '../ui.js';
|
|
7
7
|
import { useXyT, activeLocale } from '../i18n.js';
|
|
8
8
|
import { TAB_IDS } from '../../tab-policy.js';
|
|
9
|
-
import { MEMORY_LIMIT_MAX, MEMORY_LIMIT_MIN, PREF_DEFAULTS, parseMemoryLimit, } from '../../pref-policy.js';
|
|
9
|
+
import { CONFIRM_OPS, MEMORY_LIMIT_MAX, MEMORY_LIMIT_MIN, PREF_DEFAULTS, normalizeConfirmOps, parseMemoryLimit, } from '../../pref-policy.js';
|
|
10
10
|
const COACH_IDS = ['gentle', 'strict', 'humorous'];
|
|
11
11
|
const COACH_KEYS = ['settings.coach.gentle', 'settings.coach.strict', 'settings.coach.humorous'];
|
|
12
12
|
/** 显隐三态(顺序即分段按钮顺序;与 tab-policy 字面量同源)。 */
|
|
@@ -24,6 +24,15 @@ const TAB_LABEL_KEYS = {
|
|
|
24
24
|
growth: 'tab.growth',
|
|
25
25
|
memory: 'tab.memory',
|
|
26
26
|
};
|
|
27
|
+
/** 确认类目的显示名键(与 pref-policy CONFIRM_OPS 一一对应)。 */
|
|
28
|
+
const CONFIRM_OP_LABEL_KEYS = {
|
|
29
|
+
create: 'settings.pref.confirmOps.create',
|
|
30
|
+
checkin: 'settings.pref.confirmOps.checkin',
|
|
31
|
+
cancelCheckin: 'settings.pref.confirmOps.cancelCheckin',
|
|
32
|
+
claim: 'settings.pref.confirmOps.claim',
|
|
33
|
+
update: 'settings.pref.confirmOps.update',
|
|
34
|
+
memorySave: 'settings.pref.confirmOps.memorySave',
|
|
35
|
+
};
|
|
27
36
|
export function SettingsSection(props) {
|
|
28
37
|
const t = useXyT();
|
|
29
38
|
const scope = props.scope;
|
|
@@ -52,6 +61,8 @@ export function SettingsSection(props) {
|
|
|
52
61
|
const [savedMsg, setSavedMsg] = useState('');
|
|
53
62
|
// 二次确认开关:本地乐观值(undefined=跟随远端快照)——写入在途时 UI 立即响应,失败回滚
|
|
54
63
|
const [pendingToggle, setPendingToggle] = useState(undefined);
|
|
64
|
+
// 确认类目明细:乐观值为「合并后的完整对象」(每次写整体下发),失败回滚
|
|
65
|
+
const [pendingOps, setPendingOps] = useState(undefined);
|
|
55
66
|
// 标签页显隐:模式与勾选各自乐观(写入在途时禁用对应控件,失败回滚 + toast)
|
|
56
67
|
const [pendingMode, setPendingMode] = useState(undefined);
|
|
57
68
|
const [pendingHidden, setPendingHidden] = useState(undefined);
|
|
@@ -67,6 +78,8 @@ export function SettingsSection(props) {
|
|
|
67
78
|
// 开关显示值:乐观本地值优先(写入在途),否则远端快照,值缺席回落 schema 默认。
|
|
68
79
|
// 不可写成 `!== false`——那样「值未知」会被渲染成「已开启」,安全策略类开关尤其不能撒谎。
|
|
69
80
|
const confirmWrites = pendingToggle ?? snap.value?.confirmWrites ?? PREF_DEFAULTS.confirmWrites;
|
|
81
|
+
// 确认类目显示值:乐观优先 → 快照(脏键容错回落默认)→ 全默认
|
|
82
|
+
const confirmOps = pendingOps ?? normalizeConfirmOps(snap.value?.confirmOps);
|
|
70
83
|
// 偏好区提示:判定顺序不可换——memory 模式下 status 同样是 unavailable,
|
|
71
84
|
// 先判 mode 才不会把「远程/临时模式」误报成「命名空间未就绪」
|
|
72
85
|
const prefNoticeKey = snap.status === 'loading' ? 'settings.pref.loading'
|
|
@@ -165,7 +178,13 @@ export function SettingsSection(props) {
|
|
|
165
178
|
const verifyWritten = (seq, field, value) => {
|
|
166
179
|
if (seq !== writeSeqRef.current)
|
|
167
180
|
return;
|
|
168
|
-
|
|
181
|
+
const current = scope.getSnapshot().value?.[field];
|
|
182
|
+
// 对象字段(confirmOps)按 CONFIRM_OPS 逐键比对(对键序不敏感;JSON.stringify
|
|
183
|
+
// 比对会依赖两侧对象键序巧合一致);标量直接全等
|
|
184
|
+
const same = typeof value === 'object'
|
|
185
|
+
? CONFIRM_OPS.every((key) => current?.[key] === value[key])
|
|
186
|
+
: current === value;
|
|
187
|
+
if (!same)
|
|
169
188
|
toastError(new Error(t('settings.pref.writeFailed')));
|
|
170
189
|
};
|
|
171
190
|
/** 标签页显隐命名空间的写后校验(与 verifyWritten 同款,命名空间独立故写队列与序号独立)。 */
|
|
@@ -195,6 +214,26 @@ export function SettingsSection(props) {
|
|
|
195
214
|
toastError(err);
|
|
196
215
|
});
|
|
197
216
|
};
|
|
217
|
+
/**
|
|
218
|
+
* 切换单个确认类目:整体写合并后的 confirmOps 对象(与 hiddenTabs 整组写入同款),
|
|
219
|
+
* 乐观值顶住回显、失败回滚 + toast。总开关关闭时明细行禁用(不弹卡由总开关决定)。
|
|
220
|
+
*/
|
|
221
|
+
const toggleConfirmOp = (op, next) => {
|
|
222
|
+
if (!writable || pendingOps !== undefined || !confirmWrites || confirmOps[op] === next)
|
|
223
|
+
return;
|
|
224
|
+
const merged = { ...confirmOps, [op]: next };
|
|
225
|
+
setPendingOps(merged);
|
|
226
|
+
const seq = ++writeSeqRef.current;
|
|
227
|
+
void scope.set('confirmOps', merged)
|
|
228
|
+
.then(() => {
|
|
229
|
+
setPendingOps(undefined);
|
|
230
|
+
verifyWritten(seq, 'confirmOps', merged);
|
|
231
|
+
})
|
|
232
|
+
.catch((err) => {
|
|
233
|
+
setPendingOps(undefined);
|
|
234
|
+
toastError(err);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
198
237
|
const commitLimit = () => {
|
|
199
238
|
const raw = limitDraft;
|
|
200
239
|
setLimitDraft(null);
|
|
@@ -319,7 +358,32 @@ export function SettingsSection(props) {
|
|
|
319
358
|
toastError(err);
|
|
320
359
|
});
|
|
321
360
|
},
|
|
322
|
-
}), t('settings.pref.confirmWrites')), createElement('p', { className: 'xy-hint' }, t('settings.pref.confirmWritesHint'))),
|
|
361
|
+
}), t('settings.pref.confirmWrites')), createElement('p', { className: 'xy-hint' }, t('settings.pref.confirmWritesHint'))),
|
|
362
|
+
// 确认类目明细(锁定删除行恒展示):总开关关闭时整组禁用置灰——不弹卡由总开关
|
|
363
|
+
// 决定;禁用原因由组尾 hint 承担(aria-describedby 关联),不在每行重复
|
|
364
|
+
createElement('div', {
|
|
365
|
+
className: 'xy-confirm-ops',
|
|
366
|
+
role: 'group',
|
|
367
|
+
'aria-labelledby': 'xy-confirm-ops-head',
|
|
368
|
+
'aria-describedby': 'xy-confirm-ops-hint',
|
|
369
|
+
}, createElement('span', { className: 'xy-op-group-head', id: 'xy-confirm-ops-head' }, t('settings.pref.confirmOps.group')), ...CONFIRM_OPS.map((op) => createElement('label', { className: 'xy-op-row', key: op }, createElement('input', {
|
|
370
|
+
type: 'checkbox',
|
|
371
|
+
className: 'xy-toggle',
|
|
372
|
+
name: `confirmOps.${op}`,
|
|
373
|
+
checked: confirmOps[op],
|
|
374
|
+
disabled: !writable || pendingOps !== undefined || !confirmWrites,
|
|
375
|
+
onChange: (e) => toggleConfirmOp(op, e.target.checked),
|
|
376
|
+
}), t(CONFIRM_OP_LABEL_KEYS[op]))),
|
|
377
|
+
// 锁定类目:div + 只读开关(不可交互故不用 label),说明单独一行与文字对齐
|
|
378
|
+
createElement('div', { className: 'xy-op-row xy-op-row-locked' }, createElement('input', {
|
|
379
|
+
type: 'checkbox',
|
|
380
|
+
className: 'xy-toggle',
|
|
381
|
+
name: 'confirmOps.deleteLocked',
|
|
382
|
+
checked: true,
|
|
383
|
+
readOnly: true,
|
|
384
|
+
disabled: true,
|
|
385
|
+
'aria-label': t('settings.pref.confirmOps.delete'),
|
|
386
|
+
}), createElement('span', null, t('settings.pref.confirmOps.delete'))), createElement('span', { className: 'xy-hint xy-op-locked-hint' }, t('settings.pref.confirmOps.deleteHint')), createElement('span', { className: 'xy-hint', id: 'xy-confirm-ops-hint' }, t('settings.pref.confirmOps.hint'))), createElement('label', { className: 'xy-field' }, createElement('span', { className: 'xy-field-head' }, t('settings.pref.memoryLimit')), createElement('input', {
|
|
323
387
|
type: 'number', min: MEMORY_LIMIT_MIN, max: MEMORY_LIMIT_MAX,
|
|
324
388
|
className: 'xy-input xy-input-num', name: 'memoryInjectLimit', inputMode: 'numeric',
|
|
325
389
|
'aria-label': t('settings.pref.memoryLimit'),
|
|
@@ -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,6 +34,19 @@ 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;
|
|
@@ -104,9 +117,23 @@ export function WishesPage() {
|
|
|
104
117
|
'aria-valuemax': 100,
|
|
105
118
|
'aria-valuenow': wish.progress,
|
|
106
119
|
'aria-label': progressLabel + pendingMeta,
|
|
107
|
-
}, 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,
|
|
108
|
-
|
|
109
|
-
|
|
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'))]));
|
|
110
137
|
};
|
|
111
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', {
|
|
112
139
|
// 四态计数各计各的(进行中扣除计划中与待收尾,不相套);列表渲染仍归未完成组(未完成≠进行中计数)
|
package/lib/client/styles.js
CHANGED
|
@@ -60,6 +60,9 @@ export const STYLE_TEXT = `
|
|
|
60
60
|
--xyd-on-c2:#7c4a03;
|
|
61
61
|
--xyd-on-c3:#065f46;
|
|
62
62
|
--xyd-on-dcell:#0b3d26;
|
|
63
|
+
/* warn-soft 底的前景伙伴(StaleBanner 文字):壳通用次要色对琥珀柔底浅 4.19 /
|
|
64
|
+
深 2.95,跌破 WCAG 1.4.3 AA——按 ≥4.5:1 校准的专用对(on-c2 同族暖琥珀前景) */
|
|
65
|
+
--xyd-on-warn:#7c4a03;
|
|
63
66
|
/* 阴影对(卡片悬停/toast/弹窗/英雄卡):深浅各自配档,禁止正文区裸写 rgba */
|
|
64
67
|
--xyd-shadow-card:0 2px 10px rgba(15,23,42,.07);
|
|
65
68
|
--xyd-shadow-toast:0 8px 24px -8px rgba(15,23,42,.2),0 2px 8px -2px rgba(15,23,42,.1);
|
|
@@ -109,6 +112,7 @@ body[data-ds-dark-theme]{
|
|
|
109
112
|
--xyd-on-c2:#ffd9a8;
|
|
110
113
|
--xyd-on-c3:#a7f3c9;
|
|
111
114
|
--xyd-on-dcell:#a7f3c9;
|
|
115
|
+
--xyd-on-warn:#ffd9a8;
|
|
112
116
|
--xyd-shadow-card:0 2px 10px rgba(0,0,0,.32);
|
|
113
117
|
--xyd-shadow-toast:0 8px 24px -8px rgba(0,0,0,.5),0 2px 8px -2px rgba(0,0,0,.35);
|
|
114
118
|
--xyd-shadow-modal:0 24px 48px -16px rgba(0,0,0,.55),0 4px 12px -4px rgba(0,0,0,.4);
|
|
@@ -236,9 +240,9 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
236
240
|
}
|
|
237
241
|
.xy-btn:active:not(:disabled){transform:scale(.97)}
|
|
238
242
|
.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}
|
|
243
|
+
.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
244
|
/* 触控基线:消除移动端双击缩放延迟与系统点按高亮(交互反馈统一由 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}
|
|
245
|
+
.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
246
|
/* 行内按钮:≥26px 命中目标(卡片头/操作行的紧凑主战按钮) */
|
|
243
247
|
.xy-btn-inline{margin-left:auto;padding:3px 10px;font-size:12px;min-height:26px;display:inline-flex;align-items:center}
|
|
244
248
|
/* 图标幽灵键(≥26px 方形命中目标):编辑/删除这类高频重复的行内低频动作降噪——
|
|
@@ -276,6 +280,9 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
276
280
|
.xy-banner-ok{margin-top:10px;padding:10px 12px;border-radius:var(--xyd-r-inner);background:var(--xyd-ok-soft);border:1px solid var(--xyd-ok-border);font-size:13px}
|
|
277
281
|
/* 刷新失败但旧数据仍在的诚实降级横幅:琥珀虚线面板(警示但不恐吓——动作已成功) */
|
|
278
282
|
.xy-stalerow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;padding:8px 12px;border:1px dashed var(--xyd-warn-border);border-radius:var(--xyd-r-inner);background:var(--xyd-warn-soft)}
|
|
283
|
+
/* 提示文字用琥珀柔底的专用前景对(--xyd-on-warn):次要通用色在此底上浅 4.19 / 深
|
|
284
|
+
* 2.95,深色远低于 AA;重试键保持 label-primary(对琥珀柔底 >10:1),无需处理 */
|
|
285
|
+
.xy-stalerow .xy-meta{color:var(--xyd-on-warn)}
|
|
279
286
|
.xy-stalerow .xy-btn-inline{margin-left:0}
|
|
280
287
|
|
|
281
288
|
/* ===== 分组卡(任务页状态分桶 / 今日页待打卡·已完成共用)=====
|
|
@@ -321,9 +328,17 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
|
|
|
321
328
|
.xy-wishcard:hover{border-color:var(--dsw-alias-border-l2);box-shadow:var(--xyd-shadow-card)}
|
|
322
329
|
}
|
|
323
330
|
.xy-progress-num{margin-left:auto;flex:none;color:var(--dsw-alias-label-secondary);font-size:12px;font-variant-numeric:tabular-nums}
|
|
324
|
-
/*
|
|
331
|
+
/* 任务区披露开关(默认收起,展开集合走 view-state):整行按钮继承虚线分隔(原属
|
|
332
|
+
* .xy-wishtasks 的卡内分界),箭头旋转只动 transform;按钮文本含任务数(口语化,无状态词)。 */
|
|
333
|
+
.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}
|
|
334
|
+
@media (hover:hover) and (pointer:fine){
|
|
335
|
+
.xy-wishtoggle:hover{color:var(--dsw-alias-label-primary)}
|
|
336
|
+
}
|
|
337
|
+
.xy-wishtoggle svg{flex:none;transition:transform .15s ease}
|
|
338
|
+
.xy-wishtoggle[aria-expanded='true'] svg{transform:rotate(180deg)}
|
|
339
|
+
/* 卡内子任务列表:分隔线已上移到披露开关,行留白由任务行自身 padding 承担。
|
|
325
340
|
* 结构为 [taskline, detail?, taskline, detail?] —— 相邻选择器精确命中「新任务行」边界。 */
|
|
326
|
-
.xy-wishtasks{
|
|
341
|
+
.xy-wishtasks{display:flex;flex-direction:column}
|
|
327
342
|
.xy-wishtasks>.xy-taskline+.xy-taskline,.xy-wishtasks>.xy-detail+.xy-taskline{border-top:1px solid var(--dsw-alias-border-l1)}
|
|
328
343
|
.xy-wishtasks>.xy-taskline{padding:9px 0}
|
|
329
344
|
/* 展开详情是卡内最后一个元素时收掉底部留白:卡片自身 padding 已承担收尾 */
|
|
@@ -510,6 +525,16 @@ button.xy-growth-col.xy-hover{box-shadow:inset 0 0 0 2px var(--xyd-accent-ring)}
|
|
|
510
525
|
.xy-toggle::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary);transition:transform .15s}
|
|
511
526
|
.xy-toggle:checked::after{transform:translateX(16px);background:var(--xyd-on-accent)}
|
|
512
527
|
.xy-hint{color:var(--dsw-alias-label-secondary);font-size:12px;margin:0}
|
|
528
|
+
/* 确认类目明细:缩进嵌在总开关下(与「总开关 + 明细」分层同构),行 = 开关 + 名称;
|
|
529
|
+
* 锁定行(删除)为只读展示,其说明行缩进与文字对齐(开关 38px + 间距 8px) */
|
|
530
|
+
.xy-confirm-ops{display:flex;flex-direction:column;gap:8px;margin-left:22px}
|
|
531
|
+
/* 禁用弱化必须显式写:自定义外观的 toggle 不吃原生禁用变灰,总开关关闭时明细行
|
|
532
|
+
* 否则与可用态无法区分;锁定的删除行恒禁用,常态弱化即「冻结在开启态」的语义 */
|
|
533
|
+
.xy-confirm-ops .xy-toggle:disabled{opacity:.4;cursor:default}
|
|
534
|
+
.xy-op-group-head{font-weight:600;font-size:13px}
|
|
535
|
+
.xy-op-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px}
|
|
536
|
+
.xy-op-row-locked{color:var(--dsw-alias-label-secondary);cursor:default}
|
|
537
|
+
.xy-op-locked-hint{padding-left:46px}
|
|
513
538
|
|
|
514
539
|
/* ===== 详情聚合视图 ===== */
|
|
515
540
|
/* 段间 10px 呼吸;每段内部「标签 → 内容」固定 5px 纵向节奏(label 不再与内容文字挤行)。
|
|
@@ -581,6 +606,6 @@ body[data-ds-dark-theme] .xy-dcell-checked{color:var(--xyd-on-dcell)}
|
|
|
581
606
|
@media (prefers-reduced-motion: reduce){
|
|
582
607
|
.xy-skel,.xy-toast,.xy-modal-backdrop,.xy-modal{animation:none}
|
|
583
608
|
.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}
|
|
609
|
+
.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
610
|
}
|
|
586
611
|
`;
|
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')));
|