@qnroa/qtype 0.2.4 → 0.2.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 +44 -0
- package/CHANGELOG.zh.md +34 -0
- package/dist/core/typing/engine/TypingInput.js +97 -1
- package/dist/core/typing/engine/settings.js +1 -0
- package/dist/view/assets/index-4lY5IlHh.js +224 -0
- package/dist/view/assets/{index-qklN7dNP.js → index-B7iVy4Gn.js} +2 -2
- package/dist/view/assets/{index-D2_Nabek.js → index-BKlIGFJx.js} +1 -1
- package/dist/view/assets/index-CRg6M5y-.css +1 -0
- package/dist/view/index.html +2 -2
- package/package.json +1 -1
- package/dist/view/assets/index-Cve31o1m.js +0 -219
- package/dist/view/assets/index-VPadwWzO.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,50 @@ can land in any minor bump (`0.x.0`).
|
|
|
13
13
|
|
|
14
14
|
## [Unreleased]
|
|
15
15
|
|
|
16
|
+
## [0.2.5] — 2026-09-14
|
|
17
|
+
|
|
18
|
+
Second pass on card-lifecycle correctness plus a code-typing quality-
|
|
19
|
+
of-life feature. The completion pill no longer overlaps card content,
|
|
20
|
+
navigating between done and not-done cards no longer poisons the
|
|
21
|
+
fresh card with a "already done" paint, and code cards can skip
|
|
22
|
+
leading indentation the way typing.io does.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Auto-skip indent** (Typing rules → Auto-skip indent). Every line's
|
|
26
|
+
leading spaces / tabs are pre-hit for the user — the cursor jumps to
|
|
27
|
+
the first real character on each line, so code cards no longer force
|
|
28
|
+
you to type ten spaces before every statement. On by default; toggle
|
|
29
|
+
in Settings or via the HUD ⋮ menu. Same behaviour typing.io ships as
|
|
30
|
+
its default and defended publicly against "just make me type it"
|
|
31
|
+
arguments in early user feedback. Blank lines (pure-whitespace lines)
|
|
32
|
+
are not skipped — the user still hits Enter for them.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **Completion pill overlapping card content.** The "press Enter to
|
|
36
|
+
continue" hint used to sit under the HUD as an absolute-positioned
|
|
37
|
+
pill; on code / article cards where the prompt sits right below the
|
|
38
|
+
HUD, it landed on top of the first line of content. The hint now
|
|
39
|
+
lives in a fixed-height notification strip between the stage and the
|
|
40
|
+
keyboard — same pattern Anki / Duolingo use — so it never overlays
|
|
41
|
+
content and layout doesn't shift when it appears or leaves.
|
|
42
|
+
- **Wrong card shown as "already done" after rewind then advance.**
|
|
43
|
+
Sequence: finish card 1 → auto-advance to card 2 → rewind to card 1
|
|
44
|
+
(correctly rendered as done) → advance back to card 2, which was
|
|
45
|
+
now painted green even though the user had never typed it. Root
|
|
46
|
+
cause: the route's `useEffect` for `markAllHit()` ran after `cardId`
|
|
47
|
+
changed but before the state machine's `CARD_ENTER` effect settled
|
|
48
|
+
the new `kind`, so a stale `completed` from the previous card
|
|
49
|
+
painted the fresh engine. The effect now cross-checks the
|
|
50
|
+
authoritative `cardDone` (from `useDoneState`) before calling
|
|
51
|
+
`markAllHit()`.
|
|
52
|
+
- **Auto-advance timer not scheduling after `manual → auto` mid-pass.**
|
|
53
|
+
If the user switched advance mode from manual to auto while sitting
|
|
54
|
+
in a `passed{awaitingAdvance}` state, the 700ms timer that fires
|
|
55
|
+
`STAMP_TIMEOUT` never scheduled — the effect that owns it had no
|
|
56
|
+
`advanceMode` dep and its `prev !== 'passed'` gate blocked re-entry.
|
|
57
|
+
The gate is now `stampTimerRef.current === null` (idempotent), and
|
|
58
|
+
`advanceMode` is a proper dep.
|
|
59
|
+
|
|
16
60
|
## [0.2.4] — 2026-09-08
|
|
17
61
|
|
|
18
62
|
Bug-fix + refactor pass around the ignore-punctuation setting and the
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -12,6 +12,40 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.2.5] — 2026-09-14
|
|
16
|
+
|
|
17
|
+
围绕卡片生命周期的正确性又做了一轮修复,并加入一个代码打字的
|
|
18
|
+
体验改进。完成提示条不再压卡片内容;在"已完成"和"未打"卡片
|
|
19
|
+
之间来回切,新卡不再被误标为"已完成";代码卡按 typing.io 的做法
|
|
20
|
+
自动跳过缩进。
|
|
21
|
+
|
|
22
|
+
### 新增
|
|
23
|
+
- **自动跳过缩进**(打字规则 → 自动跳过缩进)。每行开头的空格 / Tab
|
|
24
|
+
由系统自动填入,光标直接落在第一个真字符上 —— 代码卡不再逼你
|
|
25
|
+
每句前先敲十个空格。默认开启,可在设置或 HUD ⋮ 菜单里切换。
|
|
26
|
+
typing.io 默认就是这个行为,作者曾公开对"就让用户自己打空格吧"
|
|
27
|
+
这种意见做过反驳。纯空白行(只有空格 + 换行)不自动跳过,用户
|
|
28
|
+
仍需手动敲回车。
|
|
29
|
+
|
|
30
|
+
### 修复
|
|
31
|
+
- **完成提示条压卡片内容。** "按 Enter 继续"提示原来是挂在 HUD
|
|
32
|
+
下方的绝对定位气泡,在代码 / 文章类卡片(内容紧贴 HUD 下方)
|
|
33
|
+
时会盖住第一行内容。现在提示条改成打字区和键盘之间的**固定
|
|
34
|
+
高度通知条**,和 Anki / Duolingo 一样 —— 不遮内容,布局也不会
|
|
35
|
+
在提示出现 / 消失时跳动。
|
|
36
|
+
- **回退再前进后新卡被误显示为"已完成"。** 场景:打完卡片 1 →
|
|
37
|
+
自动到卡片 2 → 退回卡片 1(正确显示为完成)→ 再前进到卡片 2,
|
|
38
|
+
卡片 2 被涂满绿色,尽管从没打过。根因:route 里的
|
|
39
|
+
`markAllHit()` useEffect 在 `cardId` 变化后先跑,状态机
|
|
40
|
+
`CARD_ENTER` effect 后跑,`kind` 仍是旧的 `completed`,把新引擎
|
|
41
|
+
也涂绿了。现在这个 effect 会**同时校验** `useDoneState` 里真实
|
|
42
|
+
的 `cardDone`,不再单信任 stale `kind`。
|
|
43
|
+
- **`manual → auto` 中途切换后自动前进 timer 不触发。** 用户在
|
|
44
|
+
`passed{awaitingAdvance}` 里把前进模式从 manual 切到 auto,原本
|
|
45
|
+
700ms 定时器不启动 —— effect 的 deps 里没有 `advanceMode`,而且
|
|
46
|
+
`prev !== 'passed'` 的守卫挡住了二次进入。改成幂等守卫
|
|
47
|
+
(`stampTimerRef.current === null`)并把 `advanceMode` 加入 deps。
|
|
48
|
+
|
|
15
49
|
## [0.2.4] — 2026-09-08
|
|
16
50
|
|
|
17
51
|
围绕"忽略标点"设置和"命令面板 / HUD 菜单如何呈现设置"两件事,
|
|
@@ -26,6 +26,10 @@ export class TypingInput {
|
|
|
26
26
|
#target;
|
|
27
27
|
#targetCodepoints;
|
|
28
28
|
#length;
|
|
29
|
+
/** Per-slot flag: this slot is a leading-indent whitespace that the
|
|
30
|
+
* engine will auto-hit under `autoSkipIndent`. Computed once at
|
|
31
|
+
* construction from the target's line structure. */
|
|
32
|
+
#skipMask;
|
|
29
33
|
settings;
|
|
30
34
|
#chars;
|
|
31
35
|
#position;
|
|
@@ -53,8 +57,17 @@ export class TypingInput {
|
|
|
53
57
|
? this.#targetCodepoints.map((cp) => String.fromCodePoint(cp)).join('')
|
|
54
58
|
: text;
|
|
55
59
|
this.#length = this.#targetCodepoints.length;
|
|
60
|
+
this.#skipMask = this.settings.autoSkipIndent
|
|
61
|
+
? buildIndentSkipMask(this.#targetCodepoints)
|
|
62
|
+
: new Array(this.#length).fill(false);
|
|
56
63
|
this.#chars = this.#buildInitialChars();
|
|
57
64
|
this.#position = 0;
|
|
65
|
+
// If the target starts with indent whitespace, auto-advance past
|
|
66
|
+
// it before the user's first keystroke so the initial cursor is
|
|
67
|
+
// on the first "real" character. Subsequent lines are handled in
|
|
68
|
+
// `onKeyDown` after a newline hit lands the cursor on the next
|
|
69
|
+
// line's leading whitespace.
|
|
70
|
+
this.#autoAdvanceSkipped(0);
|
|
58
71
|
}
|
|
59
72
|
#buildInitialChars() {
|
|
60
73
|
const chars = new Array(this.#length);
|
|
@@ -103,6 +116,12 @@ export class TypingInput {
|
|
|
103
116
|
this.#chars = next;
|
|
104
117
|
if (isHit || !this.settings.stopOnError) {
|
|
105
118
|
this.#position++;
|
|
119
|
+
// After advancing, if the new position is an auto-skip indent
|
|
120
|
+
// whitespace (typical case: user just hit newline, cursor sits
|
|
121
|
+
// on the next line's leading spaces / tabs), fast-forward
|
|
122
|
+
// through those slots so the user's next keystroke lands on
|
|
123
|
+
// real code.
|
|
124
|
+
this.#autoAdvanceSkipped(timestamp);
|
|
106
125
|
}
|
|
107
126
|
this.#notify();
|
|
108
127
|
if (recovered)
|
|
@@ -122,7 +141,13 @@ export class TypingInput {
|
|
|
122
141
|
// Signal that the user is trying to rewind past start.
|
|
123
142
|
return Feedback.Restart;
|
|
124
143
|
}
|
|
125
|
-
|
|
144
|
+
// Skip back over any auto-hit indent whitespace so the user
|
|
145
|
+
// doesn't "rewind" into invisible-to-them spaces. Land on the
|
|
146
|
+
// most recent user-touched slot.
|
|
147
|
+
let target = this.#position - 1;
|
|
148
|
+
while (target > 0 && this.#skipMask[target])
|
|
149
|
+
target--;
|
|
150
|
+
this.#position = target;
|
|
126
151
|
const next = this.#chars.slice();
|
|
127
152
|
const prev = next[this.#position];
|
|
128
153
|
// Clear Hit/Miss (this slot is now pending again) but remember it
|
|
@@ -143,8 +168,35 @@ export class TypingInput {
|
|
|
143
168
|
reset() {
|
|
144
169
|
this.#chars = this.#buildInitialChars();
|
|
145
170
|
this.#position = 0;
|
|
171
|
+
this.#autoAdvanceSkipped(0);
|
|
146
172
|
this.#notify();
|
|
147
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Walk `#position` past every consecutive auto-skip indent slot,
|
|
176
|
+
* marking each as an auto-hit so the arena renders them green.
|
|
177
|
+
* `timestamp` is stamped onto the skipped chars for consistency
|
|
178
|
+
* with real hits; downstream stats ignore auto-hit timestamps for
|
|
179
|
+
* WPM computation since they weren't user keystrokes.
|
|
180
|
+
*/
|
|
181
|
+
#autoAdvanceSkipped(timestamp) {
|
|
182
|
+
if (!this.settings.autoSkipIndent)
|
|
183
|
+
return;
|
|
184
|
+
while (this.#position < this.#length &&
|
|
185
|
+
this.#skipMask[this.#position]) {
|
|
186
|
+
const idx = this.#position;
|
|
187
|
+
const expected = this.#targetCodepoints[idx];
|
|
188
|
+
const next = this.#chars.slice();
|
|
189
|
+
const prev = next[idx];
|
|
190
|
+
next[idx] = {
|
|
191
|
+
target: expected,
|
|
192
|
+
typed: expected,
|
|
193
|
+
attrs: (prev.attrs & ~(CharAttr.Miss | CharAttr.Cursor)) | CharAttr.Hit,
|
|
194
|
+
timestamp,
|
|
195
|
+
};
|
|
196
|
+
this.#chars = next;
|
|
197
|
+
this.#position++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
148
200
|
/**
|
|
149
201
|
* Mark every character as Hit — used by the route's `completed`
|
|
150
202
|
* state (a card the user finished earlier) so downstream renderers
|
|
@@ -227,6 +279,50 @@ function sameLetter(a, b) {
|
|
|
227
279
|
* to leave alone, which is exactly why every mainstream typing app
|
|
228
280
|
* hard-codes its own list.
|
|
229
281
|
*/
|
|
282
|
+
/**
|
|
283
|
+
* Mark every codepoint that is leading whitespace on its line — a run
|
|
284
|
+
* of ASCII space (0x20) or tab (0x09) at line-start, up to (but not
|
|
285
|
+
* including) the first non-whitespace character on that line. Blank
|
|
286
|
+
* lines (nothing but whitespace between two newlines) are not
|
|
287
|
+
* flagged — the user still hits Enter for them, and there is no
|
|
288
|
+
* "indent" to skip.
|
|
289
|
+
*/
|
|
290
|
+
function buildIndentSkipMask(cps) {
|
|
291
|
+
const mask = new Array(cps.length).fill(false);
|
|
292
|
+
let atLineStart = true;
|
|
293
|
+
for (let i = 0; i < cps.length; i++) {
|
|
294
|
+
const cp = cps[i];
|
|
295
|
+
if (cp === 0x0a) {
|
|
296
|
+
atLineStart = true;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (atLineStart && (cp === 0x20 || cp === 0x09)) {
|
|
300
|
+
mask[i] = true;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
atLineStart = false;
|
|
304
|
+
}
|
|
305
|
+
// Blank lines: a run of whitespace immediately followed by a
|
|
306
|
+
// newline is not "indentation" — un-flag it so the user still has
|
|
307
|
+
// to hit Enter (or space, if the line is really pure spaces) to
|
|
308
|
+
// pass. Walk each flagged run and check whether it ends at a
|
|
309
|
+
// newline before any non-whitespace.
|
|
310
|
+
for (let i = 0; i < mask.length; i++) {
|
|
311
|
+
if (!mask[i])
|
|
312
|
+
continue;
|
|
313
|
+
let j = i;
|
|
314
|
+
while (j < mask.length && mask[j])
|
|
315
|
+
j++;
|
|
316
|
+
// j is the first non-mask index after the run. If cps[j] is a
|
|
317
|
+
// newline or end-of-input, this run isn't indent — clear it.
|
|
318
|
+
if (j >= cps.length || cps[j] === 0x0a) {
|
|
319
|
+
for (let k = i; k < j; k++)
|
|
320
|
+
mask[k] = false;
|
|
321
|
+
}
|
|
322
|
+
i = j - 1;
|
|
323
|
+
}
|
|
324
|
+
return mask;
|
|
325
|
+
}
|
|
230
326
|
function isPunctuation(cp) {
|
|
231
327
|
switch (cp) {
|
|
232
328
|
// ASCII sentence / clause marks. . , ! ? ; : ' "
|