@qnroa/qtype 0.0.3 → 0.0.5

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,67 @@ can land in any minor bump (`0.x.0`).
13
13
 
14
14
  ## [Unreleased]
15
15
 
16
+ ## [0.0.5] - 2026-08-22
17
+
18
+ ### Fixed
19
+
20
+ - **Mermaid pre-render no longer fails under `npx` / hosted CI.**
21
+ `mermaidPrerender.init()` now uses `createRequire(import.meta.url).resolve('mermaid/…')`
22
+ to locate the mermaid ESM, so Node's own module resolver walks the
23
+ actual `node_modules` chain regardless of where qtype was installed
24
+ — user repo, npx cache, Cloudflare Pages build root, monorepo hoist.
25
+ The previous two hard-coded lookup paths remain as fallbacks but
26
+ are no longer the primary strategy. This fixes the
27
+ `mermaid ESM not found in node_modules` error on Cloudflare Pages
28
+ when a deck actually contains ` ```mermaid ` code blocks.
29
+ - **`publish build` now copies `material/assets/**`** into
30
+ `dist/material/assets/` verbatim. Previously the CLI only walked
31
+ `.md` files, so any image the cards referenced via
32
+ `![](/material/assets/…)` returned 404 on the deployed site.
33
+
34
+ ## [0.0.4] - 2026-08-22
35
+
36
+ ### Added
37
+
38
+ - **Site name in browser tab + header** — `qtype publish build` now
39
+ writes the site's name into `dist/index.html` (both the `<title>`
40
+ and a `data-qtype-repo-name` attribute on `<html>`). The view reads
41
+ it via `readBootConfig()` so the Header brand label reflects the
42
+ actual deck instead of a hardcoded "qtype". Precedence:
43
+ `repo.name` in `.qtype/config.json` wins, otherwise the repo
44
+ directory basename.
45
+ - **HUD is present from t=0** — the pause / hint / keyboard toggles
46
+ are now reachable the moment the typing page mounts, not after the
47
+ first keystroke. `useHudBridge` derives an initial `TypingState` +
48
+ `TypingMetrics` from the engine on mount; arena's `onStateChange`
49
+ push still overrides once real events fire.
50
+
51
+ ### Changed
52
+
53
+ - **Default sound settings** — `qtype.keySoundEnabled` and
54
+ `qtype.errorSoundEnabled` now default to `true` for new users. Both
55
+ were `false` before; existing users' saved preferences are
56
+ untouched.
57
+ - **Default AFK threshold** raised from 15 s to 60 s so a short pause
58
+ to think doesn't cost the flow. Also clarified the settings
59
+ description (mouse hover without movement counts as AFK; tab hidden
60
+ / window blur pauses immediately).
61
+
62
+ ### Fixed
63
+
64
+ - **`typing.$file.tsx` no longer synthesises HUD placeholder state**
65
+ in-line — the fix lives in `useHudBridge` where it belongs. Removes
66
+ the risk of the placeholder drifting from arena's real derivation.
67
+ - **`deriveAesKey` returns an extractable AES key** so the view can
68
+ cache the derived bytes in sessionStorage — a same-tab reload no
69
+ longer re-prompts for the password. Regression test added under
70
+ `__tests__/core/encrypt.test.ts`.
71
+ - **Docs link cleanup** — all internal `.md` links in the npm-shipped
72
+ files now go through `https://www.npmjs.com/package/@qnroa/qtype?activeTab=code&filepath=...`
73
+ URLs so users on npm can click through even when the source repo
74
+ is private. Removed dead links to `CONTRIBUTING.md` /
75
+ `architecture.md` / `TODO.md` since those aren't shipped.
76
+
16
77
  ## [0.0.1] - 2026-08-16
17
78
 
18
79
  Initial development release. Everything below is the baseline shipped
package/CHANGELOG.zh.md CHANGED
@@ -12,6 +12,57 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.0.5] - 2026-08-22
16
+
17
+ ### 修复
18
+
19
+ - **Mermaid 预渲染在 `npx` / 托管 CI 环境不再失败**。
20
+ `mermaidPrerender.init()` 改用
21
+ `createRequire(import.meta.url).resolve('mermaid/…')`,让 Node
22
+ 自己顺着 `node_modules` 链找 mermaid,不管 qtype 是装到用户仓库、
23
+ npx 缓存、Cloudflare Pages build root 还是 monorepo hoist 都能
24
+ 找到。之前的两条硬编码查找路径保留作为兜底,但不再是主要策略。
25
+ 修复了 deck 里真的有 ` ```mermaid ` 代码块时,Cloudflare Pages 上
26
+ `mermaid ESM not found in node_modules` 报错。
27
+ - **`publish build` 现在会把 `material/assets/**`** 原样拷进
28
+ `dist/material/assets/`。之前 CLI 只遍历 `.md` 文件,导致卡片
29
+ 引用的 `![](/material/assets/…)` 图片在部署后 404。
30
+
31
+ ## [0.0.4] - 2026-08-22
32
+
33
+ ### 新增
34
+
35
+ - **浏览器 tab 标题 + 左上角显示站点名** —— `qtype publish build`
36
+ 会把站点名写进 `dist/index.html`(同时改 `<title>` 和 `<html>` 上的
37
+ `data-qtype-repo-name` 属性)。view 通过 `readBootConfig()` 读它,
38
+ Header 的品牌位显示当前 deck 名而不是硬编码的 "qtype"。取值优先级:
39
+ `.qtype/config.json` 里的 `repo.name` > 仓库目录名。
40
+ - **HUD 一进打字页就显示** —— 暂停 / 提示 / 键盘切换按钮从打字页
41
+ 挂载起就可用,不再需要等用户敲第一下。`useHudBridge` 在 mount 时
42
+ 就从 engine 派生初始 `TypingState` + `TypingMetrics`;arena 的
43
+ `onStateChange` 推送开始后无缝覆盖。
44
+
45
+ ### 变更
46
+
47
+ - **默认打开按键音和错误音** —— `qtype.keySoundEnabled` 和
48
+ `qtype.errorSoundEnabled` 对新用户默认改为 `true`(之前都是
49
+ `false`)。已有用户的设置不受影响。
50
+ - **AFK 默认阈值** 从 15 秒调到 60 秒,给思考一点空间。同时把设置
51
+ 说明写清楚:鼠标悬停不动也算 AFK;切 tab 或窗口失焦会立即暂停。
52
+
53
+ ### 修复
54
+
55
+ - **`typing.$file.tsx` 不再手写 HUD 初始状态** —— 修复搬到该在的
56
+ `useHudBridge`。避免手写值和 arena 真实派生逻辑漂移。
57
+ - **`deriveAesKey` 返回可导出的 AES 密钥**,view 端才能把派生结果
58
+ 存进 sessionStorage —— 同 tab 刷新不再重问密码。回归测试见
59
+ `__tests__/core/encrypt.test.ts`。
60
+ - **文档链接整改** —— npm 发布的文件里所有内部 `.md` 链接都改成
61
+ `https://www.npmjs.com/package/@qnroa/qtype?activeTab=code&filepath=...`
62
+ 形式的 npm 链接,私仓状态下 npm 用户点了也能看到内容。删掉了指向
63
+ `CONTRIBUTING.md` / `architecture.md` / `TODO.md` 这些不发布文件
64
+ 的死链。
65
+
15
66
  ## [0.0.1] - 2026-08-16
16
67
 
17
68
  首个开发版。以下是这个版本的基线内容。
@@ -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)
@@ -38,15 +38,37 @@ export class MermaidRenderer {
38
38
  pageDark = null;
39
39
  pageLight = null;
40
40
  async init() {
41
- const candidates = [
42
- path.resolve(__dirname, '..', '..', '..', '..', '..', 'node_modules', 'mermaid', 'dist', 'mermaid.esm.min.mjs'),
43
- path.resolve(process.cwd(), 'node_modules', 'mermaid', 'dist', 'mermaid.esm.min.mjs'),
44
- ];
41
+ // Ask Node's own module resolver where mermaid actually lives. It
42
+ // walks the `node_modules` chain from this file up, so it works
43
+ // regardless of where the caller installed qtype:
44
+ //
45
+ // * user repo with `npm install @qnroa/qtype` → resolves in
46
+ // `<user>/node_modules/mermaid`
47
+ // * `npx @qnroa/qtype` on any host (Cloudflare, → resolves in
48
+ // Vercel, GitHub Pages) that hoists deps into the npx cache
49
+ // an npx cache
50
+ // * monorepo with hoisted deps → resolves in
51
+ // the hoisted root
52
+ //
53
+ // The two hard-coded fallbacks below only fire if Node's resolver
54
+ // fails entirely (older Node, exotic packaging), which shouldn't
55
+ // happen in any supported deployment.
45
56
  let mermaidEsm = null;
46
- for (const c of candidates) {
47
- if (fs.existsSync(c)) {
48
- mermaidEsm = c;
49
- break;
57
+ try {
58
+ const { createRequire } = await import('node:module');
59
+ const req = createRequire(import.meta.url);
60
+ mermaidEsm = req.resolve('mermaid/dist/mermaid.esm.min.mjs');
61
+ }
62
+ catch {
63
+ const candidates = [
64
+ path.resolve(__dirname, '..', '..', '..', '..', '..', 'node_modules', 'mermaid', 'dist', 'mermaid.esm.min.mjs'),
65
+ path.resolve(process.cwd(), 'node_modules', 'mermaid', 'dist', 'mermaid.esm.min.mjs'),
66
+ ];
67
+ for (const c of candidates) {
68
+ if (fs.existsSync(c)) {
69
+ mermaidEsm = c;
70
+ break;
71
+ }
50
72
  }
51
73
  }
52
74
  if (!mermaidEsm) {