nx-sk 0.9.0 → 0.9.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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  本文件格式参考 [Keep a Changelog](https://keepachangelog.com/),中文正文。
4
4
 
5
+ ## [0.9.1] - 2026-10-15
6
+
7
+ ### Fixed
8
+
9
+ - **设置页保存无效:面板发的 body 键被静默丢掉**。HTTP body 经 `applySpec` 只保留**声明过的** args/flags(有意严格,防止多余字段流进 service),而设置页发的是平铺的 `{exportDir: …}` / `{navGroups: …}` → 全被丢掉 → service 收到空 patch,只报一句“至少要给一项:setting set --key …”(根因离现场很远)。改为走声明过的 `set`(HTTP 形态 `{set: {k: v}}`,`spec.js` 本来就写着)。**面板「默认行为」卡的保存此前同样是坏的**,一并修好。
10
+ - **分组下拉鼠标下滑就消失**:pill 与菜单之间那 4px 空隙被鼠标穿过时判成“离开”,菜单刚展开就消失——补一块透明桥盖住空隙,并在离开后延迟 140ms 再关(斜着从 pill 划到菜单行也不闪)。
11
+ - 面板文案里两处 markdown 的 `**` 直接显示了出来(JSX 不渲染 markdown)——改为 `<b>` / 纯文本。
12
+
13
+ ### Added
14
+
15
+ - **一致性断言:视图写请求的 body 键必须在 action 里声明过**(含反证:喂一段未声明键的合成代码,断言规则确实判红)。扫全部模块 view,把“发了未声明的键 → 静默丢掉 → 下游报一个离根因很远的错”这类问题钉死在测试里。
16
+
5
17
  ## [0.9.0] - 2026-10-15
6
18
 
7
19
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx-sk",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "nx-sk · 本机个人资源管理器(server-cli-web 骨架):栏目化存储个人信息与大模型密钥,CLI 与 Web 同源,敏感字段密文落盘。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -273,7 +273,7 @@ function EditForm({ modal, onChange, onSave, onSync, onAddField, onRemoveField,
273
273
  </div>
274
274
  {full?.template ? (
275
275
  <div className="hint">
276
- 本栏目由模板「{full.template}」创建。模板后来新增的字段**不会自动**出现在这里,
276
+ 本栏目由模板「{full.template}」创建。模板后来新增的字段<b>不会自动</b>出现在这里,
277
277
  点「同步模板字段」补齐(例如求职模板新增的「附件资料」组 / 简历、头像等 file 字段)。
278
278
  </div>
279
279
  ) : null}
@@ -26,11 +26,16 @@ export default function SettingsView() {
26
26
  useEffect(() => { load(); }, [load]);
27
27
 
28
28
  const save = guard(async () => {
29
+ // 必须包在 `set` 里发:HTTP 的 body 会经 applySpec **只保留声明过的键**,
30
+ // 平铺的 `{exportDir: …}` 会被静默丢掉 → service 收到空 patch 报「至少要给一项」。
31
+ // `set` 是 setting.set 声明过的 kv flag,HTTP 形态就是 `{set: {k: v}}`。
29
32
  const body = {
30
- exportDir: form.exportDir,
31
- exportFormat: form.exportFormat,
32
- includeSecretsInExport: !!form.includeSecretsInExport,
33
- defaultSection: form.defaultSection,
33
+ set: {
34
+ exportDir: form.exportDir,
35
+ exportFormat: form.exportFormat,
36
+ includeSecretsInExport: !!form.includeSecretsInExport,
37
+ defaultSection: form.defaultSection,
38
+ },
34
39
  };
35
40
  const r = await api('/api/settings', { method: 'PATCH', body });
36
41
  toast('设置已保存');
@@ -60,7 +65,7 @@ export default function SettingsView() {
60
65
  if (naked) { toast('每个分组都要有组名', 'bad'); return; }
61
66
  const empty = groups.find((g) => !g.sections.length);
62
67
  if (empty) { toast(`分组「${empty.title}」还没有勾选栏目——要么勾一个,要么删掉这个分组`, 'bad'); return; }
63
- const r = await api('/api/settings', { method: 'PATCH', body: { navGroups: groups } });
68
+ const r = await api('/api/settings', { method: 'PATCH', body: { set: { navGroups: groups } } });
64
69
  toast(groups.length ? `已保存 ${groups.length} 个导航分组` : '已取消分组,栏目恢复平铺');
65
70
  setData({ ...data, settings: r.set });
66
71
  setGroups((r.set.navGroups || []).map((g) => ({ title: g.title, sections: [...g.sections] })));
@@ -130,7 +135,7 @@ export default function SettingsView() {
130
135
  </div>
131
136
  <div className="pad stack">
132
137
  <div className="hint">
133
- 把同域栏目收拢成一个头部入口:头部只显示组名,**悬浮展开上下列表**选具体栏目。
138
+ 把同域栏目收拢成一个头部入口:头部只显示组名,悬浮展开上下列表选具体栏目。
134
139
  不勾进任何分组的栏目保持平铺;组顺序 = 头部显示顺序;一个栏目只能属于一个分组。
135
140
  </div>
136
141
  {groups.map((g, gi) => (
@@ -156,9 +156,18 @@ function SectionPane({ sectionId }) {
156
156
  function GroupTab({ group, currentId, onGo }) {
157
157
  const [open, setOpen] = useState(false);
158
158
  const ref = useRef(null);
159
+ // 离开时**延迟**再关:鼠标斜着从 pill 划到菜单行会短暂出界,立刻关就「刚展开就消失」
160
+ const closeTimer = useRef(null);
159
161
  const active = group.sections.some((s) => s.id === currentId);
160
162
  const total = group.sections.reduce((n, s) => n + (s.count || 0), 0);
161
163
 
164
+ const cancelClose = () => {
165
+ if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
166
+ };
167
+ const openNow = () => { cancelClose(); setOpen(true); };
168
+ const closeSoon = () => { cancelClose(); closeTimer.current = setTimeout(() => setOpen(false), 140); };
169
+ useEffect(() => cancelClose, []);
170
+
162
171
  useEffect(() => {
163
172
  if (!open) return undefined;
164
173
  const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
@@ -175,10 +184,10 @@ function GroupTab({ group, currentId, onGo }) {
175
184
  <span
176
185
  className="tab-group"
177
186
  ref={ref}
178
- onMouseEnter={() => setOpen(true)}
179
- onMouseLeave={() => setOpen(false)}
187
+ onMouseEnter={openNow}
188
+ onMouseLeave={closeSoon}
180
189
  >
181
- <button className={`tab ${active ? 'active' : ''}`} aria-expanded={open} onClick={() => setOpen(true)}>
190
+ <button className={`tab ${active ? 'active' : ''}`} aria-expanded={open} onClick={openNow}>
182
191
  {group.title}
183
192
  <span className="count">{total}</span>
184
193
  <span className="caret" aria-hidden />
@@ -190,7 +199,7 @@ function GroupTab({ group, currentId, onGo }) {
190
199
  key={s.id}
191
200
  role="menuitem"
192
201
  className={`tab-menu-row ${s.id === currentId ? 'current' : ''}`}
193
- onClick={() => { setOpen(false); onGo(s.id); }}
202
+ onClick={() => { cancelClose(); setOpen(false); onGo(s.id); }}
194
203
  >
195
204
  <span className="grow nowrap">{s.title}</span>
196
205
  {typeof s.count === 'number' ? <span className="count">{s.count}</span> : null}
@@ -116,6 +116,11 @@ nav.tabs {
116
116
  border-radius: var(--radius-btn);
117
117
  box-shadow: var(--shadow-relief);
118
118
  }
119
+ /* 桥:盖住 pill 与菜单之间那几像素的「死区」——鼠标下滑穿过它时不能被判成离开,
120
+ 否则菜单刚探出头就消失(hover 菜单的经典坑) */
121
+ .tab-menu::before {
122
+ content: ''; position: absolute; left: 0; right: 0; top: -5px; height: 5px;
123
+ }
119
124
  .tab-menu-row {
120
125
  display: flex; align-items: center; gap: 10px; width: 100%;
121
126
  height: var(--row-h); padding: 0 8px;
@@ -1 +1 @@
1
- import{a as l}from"./index-Dxf_b602.js";let r=null;function o(n=!1){return!n&&r||(r=l("/api/files").then(e=>e).catch(e=>{throw r=null,e})),r}function i(){return r=null,o(!0)}function u(n){return new Promise((e,a)=>{const t=new FileReader;t.onload=()=>e(t.result),t.onerror=()=>a(new Error("读取文件失败")),t.readAsDataURL(n)})}export{u as f,o as l,i as r};
1
+ import{a as l}from"./index-BcT34EVd.js";let r=null;function o(n=!1){return!n&&r||(r=l("/api/files").then(e=>e).catch(e=>{throw r=null,e})),r}function i(){return r=null,o(!0)}function u(n){return new Promise((e,a)=>{const t=new FileReader;t.onload=()=>e(t.result),t.onerror=()=>a(new Error("读取文件失败")),t.readAsDataURL(n)})}export{u as f,o as l,i as r};