skill-family-doc-render 0.5.0 → 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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # 变更日志
2
2
 
3
+ ## 0.6.1 - 2026-10-12
4
+
5
+ ### 变更
6
+
7
+ - 按本仓联合发布合同,`skill-family-doc-render` 与 `skill-family-docs`、公开站点对齐为 0.6.1。渲染器的公开 API、命令行参数、输入合同、模板和业务实现均未改变,运行依赖仍精确锁定原版本。Foundation 0.22.0 钉扎不变。
8
+
9
+ ### 说明
10
+
11
+ - 本地源包版本为 0.6.1 候选,不代表已经正式发布。安装或使用 `npx` 前,先在官方 npm 确认 `skill-family-doc-render@0.6.1` 可用。已发布的 0.6.0 保留,不覆盖该版本。
12
+
13
+ ## 0.6.0 - 2026-10-11
14
+
15
+ ### 变更
16
+
17
+ - 与 `skill-family-docs` 和公开站点的 0.6.0 联合发布对齐版本。
18
+ - `editorial` 模板调整阅读与交互样式,包内单测同步。
19
+
20
+ ### 说明
21
+
22
+ - 安装或 `npx` 前,先在官方 npm 确认 `skill-family-doc-render@0.6.0` 可用。本源版本不等于已发布或可安装版本。
23
+
3
24
  ## 0.5.0 - 2026-09-28
4
25
 
5
26
  ### 新增
package/README.md CHANGED
@@ -16,20 +16,20 @@ publication APIs. A failure before commit removes staging and leaves the existin
16
16
  untouched. After commit, or when publication is indeterminate, staging or displaced data may
17
17
  remain for diagnosis; the renderer does not guess or roll back.
18
18
 
19
- > **Version boundary:** this source tree is version `0.5.0`. That is the local version and the
19
+ > **Version boundary:** this source tree is version `0.6.1`. That is the local version and the
20
20
  > intended joint target for the renderer, documentation plugin, and public site. Confirm this
21
21
  > exact version in the official npm registry before `npm install` or `npx`. Published `0.4.5`
22
22
  > preserves the `markdown-v1`, `editorial`, and `--check-project` behavior introduced in 0.4.0
23
- > and has no `--conclusion-output` or `--read-proof`. Those two commands belong to this 0.5.0
23
+ > and has no `--conclusion-output` or `--read-proof`. Those two commands belong to this 0.6.1
24
24
  > CLI. The examples give the command shape for that version. They leave publication, Skill
25
- > Family Hub registration, and new host-reply acceptance to the official records for `0.5.0`.
25
+ > Family Hub registration, and new host-reply acceptance to the official records for `0.6.1`.
26
26
 
27
27
  ## Install
28
28
 
29
- After confirming `skill-family-doc-render@0.5.0` in the official npm registry, install that exact version:
29
+ After confirming `skill-family-doc-render@0.6.1` in the official npm registry, install that exact version:
30
30
 
31
31
  ```sh
32
- npm install --save-exact skill-family-doc-render@0.5.0
32
+ npm install --save-exact skill-family-doc-render@0.6.1
33
33
  ```
34
34
 
35
35
  Requires Node.js `>=22.22.2 <23` (aligned with the skill-family foundation packages).
@@ -39,41 +39,41 @@ This checkout currently pins exact runtime dependencies on `marked@18.0.11`,
39
39
  Markdown lexer; the renderer applies its own restricted-format validation before passing semantic
40
40
  content to the template. The workspace profile check separately uses
41
41
  `skill-family-engineering-kit@0.22.0`. These pins describe the local source and installed
42
- packages; they do not republish `0.5.0`.
42
+ packages; they do not republish `0.6.0`.
43
43
  The 0.2.0 baseline format and exit-code changes are breaking changes; existing 0.1.x users
44
44
  must re-render their baseline before using `--check`. See the changelog for the full migration
45
45
  notes.
46
46
 
47
47
  ## CLI
48
48
 
49
- Use the exact 0.5.0 CLI for rendering, checks, coverage maintenance, the Git index assertion, and the proof commands. Confirm `skill-family-doc-render@0.5.0` in the official npm registry before running them. Published 0.4.5 can run the commands that omit `--conclusion-output` and `--read-proof`.
49
+ Use the exact 0.6.1 CLI for rendering, checks, coverage maintenance, the Git index assertion, and the proof commands. Confirm `skill-family-doc-render@0.6.1` in the official npm registry before running them. Published 0.4.5 can run the commands that omit `--conclusion-output` and `--read-proof`.
50
50
 
51
51
  ```sh
52
52
  # render all repos with a site field (writes to disk)
53
- npx skill-family-doc-render@0.5.0
53
+ npx skill-family-doc-render@0.6.1
54
54
 
55
55
  # drift check only: compare in-memory render against the on-disk site-baseline.json, write nothing
56
- npx skill-family-doc-render@0.5.0 --check
56
+ npx skill-family-doc-render@0.6.1 --check
57
57
 
58
58
  # render / check a single repo by name
59
- npx skill-family-doc-render@0.5.0 --repo <name>
59
+ npx skill-family-doc-render@0.6.1 --repo <name>
60
60
 
61
61
  # assert every rendered file is tracked in the Git index, then render
62
- npx skill-family-doc-render@0.5.0 --assert-git
62
+ npx skill-family-doc-render@0.6.1 --assert-git
63
63
 
64
64
  # run both the drift and Git index tracking checks without writing
65
- npx skill-family-doc-render@0.5.0 --check --assert-git
65
+ npx skill-family-doc-render@0.6.1 --check --assert-git
66
66
 
67
67
  # inspect or refresh the reviewed product-input snapshot
68
- npx skill-family-doc-render@0.5.0 --status --repo <name>
69
- npx skill-family-doc-render@0.5.0 --refresh-coverage --repo <name>
70
- npx skill-family-doc-render@0.5.0 --check-project --repo <name>
68
+ npx skill-family-doc-render@0.6.1 --status --repo <name>
69
+ npx skill-family-doc-render@0.6.1 --refresh-coverage --repo <name>
70
+ npx skill-family-doc-render@0.6.1 --check-project --repo <name>
71
71
 
72
72
  # after the mechanical check, exclusive-create one proof; do not overwrite
73
- npx skill-family-doc-render@0.5.0 --check-project --repo <name> --conclusion-output /absolute/path/conclusion.json
73
+ npx skill-family-doc-render@0.6.1 --check-project --repo <name> --conclusion-output /absolute/path/conclusion.json
74
74
 
75
75
  # read one existing proof from any working directory; write nothing
76
- npx skill-family-doc-render@0.5.0 --read-proof --proof-root /absolute/proof-root --proof relative/conclusion.json
76
+ npx skill-family-doc-render@0.6.1 --read-proof --proof-root /absolute/proof-root --proof relative/conclusion.json
77
77
  ```
78
78
 
79
79
  `--check-project` still requires the current directory to own `public-release.json`.
@@ -104,7 +104,7 @@ Same-version `DOCS_CLEAR` maps to `pass` only when `completion` is `complete`;
104
104
  That status keeps the proof's recorded non-pass. A missing file, damaged JSON, a different
105
105
  provider, or an unknown same-version domain code is `unavailable` (exit `2`). Earlier
106
106
  local-candidate runs exercised proof output and proof reading; those runs keep their original
107
- scene limits. Publication, Hub registration, and new host replies for `0.5.0` stay with the
107
+ scene limits. Publication, Hub registration, and new host replies for `0.6.1` stay with the
108
108
  official records. These proof commands consume the Foundation
109
109
  `professional-conclusion` contract from `skill-family-contracts@0.22.0`. Refreshing a proof
110
110
  means running `--check-project` again to a new output path. Remediation, site creation, and
@@ -318,7 +318,7 @@ Foundation packages below, it uses exact `marked@18.0.11` for Markdown tokenizat
318
318
  Apache-2.0
319
319
 
320
320
  <!-- release-skill:capability:safe-first-command -->
321
- > **Start here:** run `npx skill-family-doc-render@0.5.0 --check-project --repo <name>` after
321
+ > **Start here:** run `npx skill-family-doc-render@0.6.1 --check-project --repo <name>` after
322
322
  > confirming that exact version in the official registry. From a local checkout, stay in the
323
323
  > directory that owns `public-release.json` and call
324
324
  > `node /absolute/path/to/skill-family-doc-render/bin/skill-family-doc-render.mjs --check-project --repo <name>`,
@@ -346,6 +346,6 @@ Apache-2.0
346
346
  Display help and run a read-only project check:
347
347
 
348
348
  ```sh
349
- npx skill-family-doc-render@0.5.0 --help
350
- npx skill-family-doc-render@0.5.0 --check-project --repo <name>
349
+ npx skill-family-doc-render@0.6.1 --help
350
+ npx skill-family-doc-render@0.6.1 --check-project --repo <name>
351
351
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-family-doc-render",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Config-driven GitHub Pages knowledge-site renderer for skill-family projects (multi-repo capable).",
5
5
  "author": "广州市风荷科技有限公司",
6
6
  "license": "Apache-2.0",
@@ -99,12 +99,49 @@
99
99
  let activeDialog;
100
100
  // Release synchronously before following links or switching dialogs. The queued
101
101
  // native close event must never restore an old position over a destination.
102
+ function captureViewport() {
103
+ return { left: window.scrollX, top: window.scrollY };
104
+ }
105
+ // Sticky header controls sit in document flow at y=0. Pointerdown/keydown must
106
+ // record the viewport before focus scrolls that in-flow box into view.
107
+ function bindOpener(button) {
108
+ let viewport = null;
109
+ const remember = () => { viewport = captureViewport(); };
110
+ button.addEventListener('pointerdown', remember);
111
+ button.addEventListener('touchstart', remember, { passive: true });
112
+ button.addEventListener('keydown', event => {
113
+ if (event.key === 'Enter' || event.key === ' ') remember();
114
+ });
115
+ button.addEventListener('mousedown', event => {
116
+ remember();
117
+ event.preventDefault();
118
+ });
119
+ return () => {
120
+ const value = viewport ?? captureViewport();
121
+ viewport = null;
122
+ return value;
123
+ };
124
+ }
125
+ function restoreScroll(state) {
126
+ const html = document.documentElement;
127
+ const previous = html.style.scrollBehavior;
128
+ html.style.scrollBehavior = 'auto';
129
+ const apply = () => window.scrollTo({ left: state.left, top: state.top, behavior: 'auto' });
130
+ apply();
131
+ requestAnimationFrame(() => {
132
+ apply();
133
+ requestAnimationFrame(apply);
134
+ });
135
+ setTimeout(apply, 0);
136
+ html.style.scrollBehavior = previous;
137
+ }
102
138
  function releaseDialog(state, returnFocus = true) {
103
139
  if (activeDialog !== state) return;
104
140
  activeDialog = null;
141
+ document.documentElement.classList.remove('modal-open');
105
142
  document.body.classList.remove('modal-open');
106
143
  document.body.style.removeProperty('top');
107
- window.scrollTo({ left: state.left, top: state.top, behavior: 'instant' });
144
+ restoreScroll(state);
108
145
  if (returnFocus && state.invoker?.isConnected) state.invoker.focus({ preventScroll: true });
109
146
  }
110
147
  function closeDialog(dialog, returnFocus = true) {
@@ -112,11 +149,17 @@
112
149
  dialog.close();
113
150
  if (state?.dialog === dialog) releaseDialog(state, returnFocus);
114
151
  }
115
- function showDialog(dialog, invoker) {
152
+ function showDialog(dialog, invoker, viewport) {
116
153
  if (activeDialog) closeDialog(activeDialog.dialog);
117
- const state = { dialog, invoker, left: window.scrollX, top: window.scrollY };
154
+ const state = {
155
+ dialog,
156
+ invoker,
157
+ left: viewport?.left ?? window.scrollX,
158
+ top: viewport?.top ?? window.scrollY,
159
+ };
118
160
  activeDialog = state;
119
161
  document.body.style.top = `${-state.top}px`;
162
+ document.documentElement.classList.add('modal-open');
120
163
  document.body.classList.add('modal-open');
121
164
  try { dialog.showModal(); } catch (error) { releaseDialog(state); throw error; }
122
165
  }
@@ -130,7 +173,8 @@
130
173
  if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) closeDialog(dialog);
131
174
  });
132
175
  }
133
- get('menu-toggle').addEventListener('click', () => showDialog(menu, get('menu-toggle')));
176
+ const menuViewport = bindOpener(get('menu-toggle'));
177
+ get('menu-toggle').addEventListener('click', () => showDialog(menu, get('menu-toggle'), menuViewport()));
134
178
  get('close-menu').addEventListener('click', () => closeDialog(menu));
135
179
  menu.addEventListener('click', event => {
136
180
  if (event.target.closest('a')) closeDialog(menu, false);
@@ -189,14 +233,15 @@
189
233
  setActiveResult(0, false);
190
234
  results.scrollTop = 0;
191
235
  }
192
- function openSearch(invoker) {
236
+ const searchViewport = bindOpener(searchToggle);
237
+ function openSearch(invoker, viewport) {
193
238
  if (invoker?.closest('dialog')) invoker = searchToggle;
194
239
  input.value = '';
195
240
  updateSearch();
196
- showDialog(searchDialog, invoker);
241
+ showDialog(searchDialog, invoker, viewport);
197
242
  input.focus({ preventScroll: true });
198
243
  }
199
- searchToggle.addEventListener('click', () => openSearch(searchToggle));
244
+ searchToggle.addEventListener('click', () => openSearch(searchToggle, searchViewport()));
200
245
  get('close-search').addEventListener('click', () => closeDialog(searchDialog));
201
246
  input.addEventListener('input', updateSearch);
202
247
  input.addEventListener('keydown', event => {
@@ -6,7 +6,7 @@
6
6
  --surface: #fff;
7
7
  --ink: #26392f;
8
8
  --text: #3d4b43;
9
- --muted: #657168;
9
+ --muted: #5f6c62; /* muted on tint must stay ≥4.5:1; #657168 was 4.394 */
10
10
  --line: #dce3d9;
11
11
  --green: #2f634c;
12
12
  --tint: #e9f0e7;
@@ -122,8 +122,10 @@ main:focus:not(:focus-visible) { outline: none; }
122
122
  .breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; color: var(--muted); font-size: 11px; margin: 0 auto 35px; max-width: 740px; }
123
123
  .breadcrumb > span:last-child { color: var(--text); }
124
124
  article { max-width: 740px; margin: 0 auto; }
125
- .intro { font-size: 15px; line-height: 1.9; color: var(--muted); margin-bottom: 20px; max-width: 44em; }
125
+ article.page-home { overflow-x: clip; }
126
+ .intro { font-size: 16px; line-height: 1.8; color: var(--muted); margin-bottom: 20px; max-width: 44em; }
126
127
  .page-heading { margin-bottom: 30px; }
128
+ .page-heading .text-action { display: inline-flex; margin: -6px 0 14px; }
127
129
  .content-list { margin: 20px 0; padding-left: 1.5em; }
128
130
  .content-list li { padding-left: .3em; margin: 12px 0; }
129
131
  .heading-link { font-size: .7em; text-decoration: none; font-weight: 400; padding: 4px; opacity: .65; }
@@ -142,9 +144,9 @@ article { max-width: 740px; margin: 0 auto; }
142
144
  .article-footer a { text-decoration: none; white-space: nowrap; }
143
145
 
144
146
  /* Home page uses the same manifest-driven task sequence as the pager. */
145
- .home-hero { display: grid; grid-template-columns: minmax(0,1fr) 180px; align-items: center; gap: 25px; }
147
+ .home-hero { display: grid; grid-template-columns: minmax(0,1fr) 180px; align-items: center; gap: 25px; overflow: hidden; }
146
148
  .home-hero h1 { font-size: 38px; }
147
- .home-hero .intro { font-size: 14px; }
149
+ .home-hero .intro { font-size: 16px; }
148
150
  .hero-actions { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin: 24px 0 12px; }
149
151
  .hero-actions:empty { display: none; }
150
152
  .button { display: inline-flex; align-items: center; justify-content: space-between; gap: 28px; text-decoration: none; border-radius: 7px; padding: 10px 14px 10px 18px; font-size: 13px; font-weight: 500; transition: transform 160ms var(--ease), background 160ms var(--ease); }
@@ -172,8 +174,8 @@ article { max-width: 740px; margin: 0 auto; }
172
174
  .route-list > a:first-child { border-top: 0; }
173
175
  .route-list > a:hover { background: var(--tint); padding-left: 10px; padding-right: 10px; }
174
176
  .route-number { font-size: 11px; color: var(--muted); align-self: flex-start; padding-top: 4px; flex: none; }
175
- .route-list h3 { font-size: 14px; margin: 0 0 3px; }
176
- .route-list p { font-size: 12px; color: var(--muted); margin: 0; line-height: 1.8; }
177
+ .route-list h3 { font-size: 16px; margin: 0 0 3px; }
178
+ .route-list p { font-size: 16px; color: var(--muted); margin: 0; line-height: 1.8; }
177
179
  .route-arrow { margin-left: auto; font-size: 18px; font-weight: 400; color: var(--green); flex: none; }
178
180
 
179
181
  /* Semantic blocks; no project-provided layout classes. */
@@ -208,7 +210,7 @@ td code { white-space: nowrap; }
208
210
  dialog { background: var(--surface); color: var(--text); border: 1px solid var(--line); border-radius: 12px; padding: 0; box-shadow: var(--shadow); }
209
211
  dialog:not([open]) { display: none; }
210
212
  dialog::backdrop { background: #162c2455; backdrop-filter: blur(3px); }
211
- html:has(dialog[open]) { overflow: hidden; }
213
+ html.modal-open { overflow: hidden; }
212
214
  body.modal-open { position: fixed; left: 0; right: 0; width: 100%; }
213
215
  #editorial-search-dialog { width: min(600px,calc(100vw - 28px)); max-height: 75dvh; margin: 12dvh auto auto; }
214
216
  #editorial-search-dialog[open] { display: flex; flex-direction: column; }
@@ -238,7 +240,6 @@ body.modal-open { position: fixed; left: 0; right: 0; width: 100%; }
238
240
  .sidebar { padding-left: 32px; }
239
241
  .home-hero { grid-template-columns: minmax(0,1fr) 200px; gap: 35px; }
240
242
  .home-hero h1 { font-size: 42px; }
241
- .home-hero .intro { font-size: 15px; }
242
243
  }
243
244
  @media (max-width: 1199px) {
244
245
  .layout { grid-template-columns: 210px minmax(0,1fr); }
@@ -269,7 +270,6 @@ body.modal-open { position: fixed; left: 0; right: 0; width: 100%; }
269
270
  .home-hero { grid-template-columns: minmax(0,1fr) 155px; gap: 20px; }
270
271
  .home-hero h1 { font-size: 34px; }
271
272
  h1 { font-size: 31px; }
272
- .intro { font-size: 14px; }
273
273
  .hero-actions { gap: 17px; }
274
274
  .page-heading { margin-bottom: 24px; }
275
275
  .article-footer { margin-top: 42px; }
@@ -278,14 +278,11 @@ body.modal-open { position: fixed; left: 0; right: 0; width: 100%; }
278
278
  .home-hero { display: block; }
279
279
  .source-illustration { display: none; }
280
280
  .home-hero h1 { font-size: 34px; line-height: 1.42; }
281
- .home-hero .intro { font-size: 14px; }
282
281
  .hero-actions { gap: 18px; margin-top: 22px; }
283
282
  h1 { font-size: 29px; letter-spacing: -.04em; }
284
283
  h2 { font-size: 21px; }
285
284
  .start-section { padding-top: 23px; margin-top: 30px; }
286
285
  .route-list > a { padding: 18px 0; gap: 12px; }
287
- .route-list h3 { font-size: 14px; }
288
- .route-list p { font-size: 12px; }
289
286
  .route-arrow { font-size: 16px; }
290
287
  .route-number { font-size: 10px; }
291
288
  .callout { padding: 14px; gap: 10px; font-size: 12px; }
@@ -313,7 +310,7 @@ body.modal-open { position: fixed; left: 0; right: 0; width: 100%; }
313
310
  }
314
311
  .site-header, .sidebar, .page-outline, .copy-button, .heading-copy, .heading-link,
315
312
  .page-pager, .article-footer, .toast, dialog, .skip-link, .mobile-fallback { display: none !important; }
316
- html:has(dialog[open]) { overflow: visible; }
313
+ html.modal-open { overflow: visible; }
317
314
  body.modal-open { position: static; top: auto !important; }
318
315
  .layout { display: block; }
319
316
  main { padding: 0; }
@@ -201,11 +201,13 @@ function tutorial(model, page) {
201
201
  return `<nav class="page-pager" aria-label="教程翻页">${previous ? `<a class="pager-previous" href="${pageHref(previous)}" rel="prev"><span>上一步</span><strong>← ${escape(label(previous))}</strong></a>` : ''}${next ? `<a class="pager-next" href="${pageHref(next)}" rel="next"><span>下一步</span><strong>${escape(label(next))} →</strong></a>` : ''}</nav>`;
202
202
  }
203
203
  function hero(model, page) {
204
- const title = `<h1>${escape(page.title)}</h1><p class="intro">${renderInline(page.blocks[0].content)}</p>`;
204
+ const intro = `<p class="intro">${renderInline(page.blocks[0].content)}</p>`;
205
205
  if (page.kind !== 'home') {
206
206
  const promptIndex = page.kind === 'task' ? page.blocks.findIndex(block => block.type === 'code' && block.language === 'prompt') : -1;
207
- return `<div class="page-heading">${title}${promptIndex >= 0 ? `<a class="text-action" href="#editorial-code-${promptIndex}">查看操作提示词 <span aria-hidden="true">↓</span></a>` : ''}</div>`;
207
+ const jump = promptIndex >= 0 ? `<a class="text-action" href="#editorial-code-${promptIndex}">查看操作提示词 <span aria-hidden="true">↓</span></a>` : '';
208
+ return `<div class="page-heading"><h1>${escape(page.title)}</h1>${jump}${intro}</div>`;
208
209
  }
210
+ const title = `<h1>${escape(page.title)}</h1>${intro}`;
209
211
  const tasks = model.pages.filter(item => item.inPager);
210
212
  const first = tasks[0];
211
213
  const maintenance = tasks.find(item => item.role === 'maintenance' && item !== first);