@qnroa/qtype 0.0.3 → 0.0.4

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
@@ -13,6 +13,49 @@ can land in any minor bump (`0.x.0`).
13
13
 
14
14
  ## [Unreleased]
15
15
 
16
+ ## [0.0.4] - 2026-08-22
17
+
18
+ ### Added
19
+
20
+ - **Site name in browser tab + header** — `qtype publish build` now
21
+ writes the site's name into `dist/index.html` (both the `<title>`
22
+ and a `data-qtype-repo-name` attribute on `<html>`). The view reads
23
+ it via `readBootConfig()` so the Header brand label reflects the
24
+ actual deck instead of a hardcoded "qtype". Precedence:
25
+ `repo.name` in `.qtype/config.json` wins, otherwise the repo
26
+ directory basename.
27
+ - **HUD is present from t=0** — the pause / hint / keyboard toggles
28
+ are now reachable the moment the typing page mounts, not after the
29
+ first keystroke. `useHudBridge` derives an initial `TypingState` +
30
+ `TypingMetrics` from the engine on mount; arena's `onStateChange`
31
+ push still overrides once real events fire.
32
+
33
+ ### Changed
34
+
35
+ - **Default sound settings** — `qtype.keySoundEnabled` and
36
+ `qtype.errorSoundEnabled` now default to `true` for new users. Both
37
+ were `false` before; existing users' saved preferences are
38
+ untouched.
39
+ - **Default AFK threshold** raised from 15 s to 60 s so a short pause
40
+ to think doesn't cost the flow. Also clarified the settings
41
+ description (mouse hover without movement counts as AFK; tab hidden
42
+ / window blur pauses immediately).
43
+
44
+ ### Fixed
45
+
46
+ - **`typing.$file.tsx` no longer synthesises HUD placeholder state**
47
+ in-line — the fix lives in `useHudBridge` where it belongs. Removes
48
+ the risk of the placeholder drifting from arena's real derivation.
49
+ - **`deriveAesKey` returns an extractable AES key** so the view can
50
+ cache the derived bytes in sessionStorage — a same-tab reload no
51
+ longer re-prompts for the password. Regression test added under
52
+ `__tests__/core/encrypt.test.ts`.
53
+ - **Docs link cleanup** — all internal `.md` links in the npm-shipped
54
+ files now go through `https://www.npmjs.com/package/@qnroa/qtype?activeTab=code&filepath=...`
55
+ URLs so users on npm can click through even when the source repo
56
+ is private. Removed dead links to `CONTRIBUTING.md` /
57
+ `architecture.md` / `TODO.md` since those aren't shipped.
58
+
16
59
  ## [0.0.1] - 2026-08-16
17
60
 
18
61
  Initial development release. Everything below is the baseline shipped
package/CHANGELOG.zh.md CHANGED
@@ -12,6 +12,41 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.0.4] - 2026-08-22
16
+
17
+ ### 新增
18
+
19
+ - **浏览器 tab 标题 + 左上角显示站点名** —— `qtype publish build`
20
+ 会把站点名写进 `dist/index.html`(同时改 `<title>` 和 `<html>` 上的
21
+ `data-qtype-repo-name` 属性)。view 通过 `readBootConfig()` 读它,
22
+ Header 的品牌位显示当前 deck 名而不是硬编码的 "qtype"。取值优先级:
23
+ `.qtype/config.json` 里的 `repo.name` > 仓库目录名。
24
+ - **HUD 一进打字页就显示** —— 暂停 / 提示 / 键盘切换按钮从打字页
25
+ 挂载起就可用,不再需要等用户敲第一下。`useHudBridge` 在 mount 时
26
+ 就从 engine 派生初始 `TypingState` + `TypingMetrics`;arena 的
27
+ `onStateChange` 推送开始后无缝覆盖。
28
+
29
+ ### 变更
30
+
31
+ - **默认打开按键音和错误音** —— `qtype.keySoundEnabled` 和
32
+ `qtype.errorSoundEnabled` 对新用户默认改为 `true`(之前都是
33
+ `false`)。已有用户的设置不受影响。
34
+ - **AFK 默认阈值** 从 15 秒调到 60 秒,给思考一点空间。同时把设置
35
+ 说明写清楚:鼠标悬停不动也算 AFK;切 tab 或窗口失焦会立即暂停。
36
+
37
+ ### 修复
38
+
39
+ - **`typing.$file.tsx` 不再手写 HUD 初始状态** —— 修复搬到该在的
40
+ `useHudBridge`。避免手写值和 arena 真实派生逻辑漂移。
41
+ - **`deriveAesKey` 返回可导出的 AES 密钥**,view 端才能把派生结果
42
+ 存进 sessionStorage —— 同 tab 刷新不再重问密码。回归测试见
43
+ `__tests__/core/encrypt.test.ts`。
44
+ - **文档链接整改** —— npm 发布的文件里所有内部 `.md` 链接都改成
45
+ `https://www.npmjs.com/package/@qnroa/qtype?activeTab=code&filepath=...`
46
+ 形式的 npm 链接,私仓状态下 npm 用户点了也能看到内容。删掉了指向
47
+ `CONTRIBUTING.md` / `architecture.md` / `TODO.md` 这些不发布文件
48
+ 的死链。
49
+
15
50
  ## [0.0.1] - 2026-08-16
16
51
 
17
52
  首个开发版。以下是这个版本的基线内容。
@@ -42,6 +42,10 @@ export async function publishBuildAction(opts = {}) {
42
42
  const encryptCtx = await resolveEncryptCtx(cwd);
43
43
  const mermaidMode = await resolveMermaidMode(cwd);
44
44
  const repoLang = await resolveRepoLang(cwd);
45
+ // Wiki display name: repo.name from config wins over the directory
46
+ // basename, so users can `qtype config set repo.name "..."` to
47
+ // override how the site labels itself in the browser tab.
48
+ const name = await resolveRepoName(cwd);
45
49
  const materials = [];
46
50
  for (const file of walkMaterial(cwd)) {
47
51
  const body = readMaterial(file);
@@ -64,6 +68,11 @@ export async function publishBuildAction(opts = {}) {
64
68
  fs.rmSync(outDir, { recursive: true, force: true });
65
69
  fs.mkdirSync(outDir, { recursive: true });
66
70
  copyDir(viewBundle, outDir);
71
+ // Rewrite dist/index.html to carry the site's name — both in the
72
+ // <title> (browser tab) and as a `data-qtype-repo-name` attribute on
73
+ // <html> (read by the view's `readBootConfig()` for the header
74
+ // brand label and anywhere else the repo name is shown).
75
+ rewriteIndexHtml(path.join(outDir, 'index.html'), name);
67
76
  // Defensive: clear any stale material/ that might have snuck into the
68
77
  // view bundle. Since dev-seed content lives at repo root's examples/
69
78
  // (served only by the vite dev middleware, not the bundle), this is a
@@ -107,6 +116,14 @@ export async function publishBuildAction(opts = {}) {
107
116
  fs.writeFileSync(path.join(targetMaterial, 'mermaid.css'), mermaid.css);
108
117
  }
109
118
  }
119
+ // Copy `material/assets/**` verbatim (images referenced by cards
120
+ // via `![](/material/assets/…)` need to actually resolve at the
121
+ // deployed URL). Assets are NOT encrypted — they're image bytes,
122
+ // and the encryption pass only wraps `.md` content anyway.
123
+ const srcAssets = path.join(cwd, MATERIAL_DIR, 'assets');
124
+ if (fs.existsSync(srcAssets)) {
125
+ copyDir(srcAssets, path.join(targetMaterial, 'assets'));
126
+ }
110
127
  // Manifest: lists materials + optional encryption envelope + mermaid mode.
111
128
  const manifest = { materials: entries };
112
129
  if (encryptCtx) {
@@ -186,6 +203,38 @@ async function resolveRepoLang(cwd) {
186
203
  return 'zh-CN';
187
204
  return 'en-US';
188
205
  }
206
+ /**
207
+ * Repo display name: `repo.name` from `.qtype/config.json` wins over
208
+ * the directory basename, so users can override without renaming the
209
+ * folder. Same rule as qrism's `loadPublishConfig`.
210
+ */
211
+ async function resolveRepoName(cwd) {
212
+ const kv = createRepoKv(cwd);
213
+ if (await kv.available()) {
214
+ const v = await kv.get('repo.name');
215
+ if (typeof v === 'string' && v.trim())
216
+ return v.trim();
217
+ }
218
+ return path.basename(cwd);
219
+ }
220
+ function rewriteIndexHtml(indexPath, name) {
221
+ if (!fs.existsSync(indexPath))
222
+ return;
223
+ const html = fs.readFileSync(indexPath, 'utf8');
224
+ const escaped = name
225
+ .replace(/&/g, '&amp;')
226
+ .replace(/</g, '&lt;')
227
+ .replace(/>/g, '&gt;')
228
+ .replace(/"/g, '&quot;');
229
+ const withTitle = html.replace(/<title>[^<]*<\/title>/, `<title>${escaped}</title>`);
230
+ // Add or replace data-qtype-repo-name on the root <html> tag.
231
+ // Two branches so a re-run cleanly overwrites an existing attribute.
232
+ const rewritten = /<html\b[^>]*\bdata-qtype-repo-name=/.test(withTitle)
233
+ ? withTitle.replace(/(<html\b[^>]*\bdata-qtype-repo-name=)"[^"]*"/, `$1"${escaped}"`)
234
+ : withTitle.replace(/<html\b/, `<html data-qtype-repo-name="${escaped}"`);
235
+ if (rewritten !== html)
236
+ fs.writeFileSync(indexPath, rewritten);
237
+ }
189
238
  async function obtainPassword() {
190
239
  const envPwd = process.env.QTYPE_PUBLISH_PASSWORD;
191
240
  if (envPwd)