qwerty-cli 0.0.1-alpha.7 → 0.0.1-alpha.8

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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { Command as Command7 } from "commander";
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "qwerty-cli",
7
- version: "0.0.1-alpha.7",
7
+ version: "0.0.1-alpha.8",
8
8
  description: "Terminal clone of qwerty-learner: typing practice for English vocabulary, with chapters, dictation, mistake book, and audio.",
9
9
  type: "module",
10
10
  bin: {
@@ -1018,6 +1018,9 @@ var en = {
1018
1018
  chapterDone: "chapter done",
1019
1019
  resumeHint: "Enter resume \xB7 Esc menu",
1020
1020
  nextHint: "Enter next \xB7 Esc menu",
1021
+ pausedHintRight: "Enter resume",
1022
+ nextHintRight: "Enter next",
1023
+ infoChipLabel: "info",
1021
1024
  infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
1022
1025
  }
1023
1026
  };
@@ -1243,6 +1246,9 @@ var zh = {
1243
1246
  chapterDone: "chapter done",
1244
1247
  resumeHint: "Enter resume \xB7 Esc menu",
1245
1248
  nextHint: "Enter next \xB7 Esc menu",
1249
+ pausedHintRight: "Enter \u7EE7\u7EED",
1250
+ nextHintRight: "Enter \u4E0B\u4E00\u7AE0",
1251
+ infoChipLabel: "\u4FE1\u606F",
1246
1252
  infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
1247
1253
  }
1248
1254
  };
@@ -1339,8 +1345,8 @@ import { useState as useState6 } from "react";
1339
1345
  import { Box as Box3, Text as Text2, useApp, useInput } from "ink";
1340
1346
 
1341
1347
  // src/ui/components/BigWord.tsx
1342
- import { Box as Box2, Text, useStdout as useStdout2 } from "ink";
1343
- import { jsx as jsx7, jsxs } from "react/jsx-runtime";
1348
+ import { Box as Box2, Text } from "ink";
1349
+ import { jsx as jsx7 } from "react/jsx-runtime";
1344
1350
  var PALETTE = {
1345
1351
  accent: "#5eead4",
1346
1352
  muted: "#6b7280",
@@ -1351,31 +1357,14 @@ var PALETTE = {
1351
1357
  error: "#f87171"
1352
1358
  };
1353
1359
  function BigWord({ target, typed, error = false, hideTarget = false }) {
1354
- const { stdout } = useStdout2();
1355
- const cols = stdout?.columns ?? 80;
1356
1360
  const chars = [...target];
1357
1361
  const typedChars = [...typed];
1358
- const sep = cols >= 80 ? " " : cols >= 60 ? " " : " ";
1359
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", alignItems: "center", paddingY: 1, children: [
1360
- /* @__PURE__ */ jsx7(Box2, { children: chars.map((ch, i) => {
1361
- const isTyped = i < typedChars.length;
1362
- const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
1363
- const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;
1364
- return /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
1365
- display,
1366
- i < chars.length - 1 ? sep : ""
1367
- ] }, i);
1368
- }) }),
1369
- /* @__PURE__ */ jsx7(Box2, { children: chars.map((ch, i) => {
1370
- const isTyped = i < typedChars.length;
1371
- const trackChar = isTyped ? "\u2501" : "\u2500";
1372
- const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;
1373
- return /* @__PURE__ */ jsxs(Text, { color, children: [
1374
- trackChar,
1375
- i < chars.length - 1 ? sep : ""
1376
- ] }, i);
1377
- }) })
1378
- ] });
1362
+ return /* @__PURE__ */ jsx7(Box2, { paddingY: 4, justifyContent: "center", children: chars.map((ch, i) => {
1363
+ const isTyped = i < typedChars.length;
1364
+ const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
1365
+ const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;
1366
+ return /* @__PURE__ */ jsx7(Text, { bold: true, color, children: display }, i);
1367
+ }) });
1379
1368
  }
1380
1369
 
1381
1370
  // src/util/text.ts
@@ -1409,7 +1398,7 @@ function truncateName(name, max) {
1409
1398
  }
1410
1399
 
1411
1400
  // src/ui/screens/MainMenu.tsx
1412
- import { jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
1401
+ import { jsx as jsx8, jsxs } from "react/jsx-runtime";
1413
1402
  function MainMenu({ cfg }) {
1414
1403
  const [selected, setSelected] = useState6(0);
1415
1404
  const { exit } = useApp();
@@ -1479,10 +1468,10 @@ function MainMenu({ cfg }) {
1479
1468
  }
1480
1469
  }
1481
1470
  });
1482
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", children: [
1483
- /* @__PURE__ */ jsxs2(Box3, { children: [
1471
+ return /* @__PURE__ */ jsxs(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", children: [
1472
+ /* @__PURE__ */ jsxs(Box3, { children: [
1484
1473
  /* @__PURE__ */ jsx8(Text2, { bold: true, color: PALETTE.accent, children: t.app.title }),
1485
- /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
1474
+ /* @__PURE__ */ jsxs(Text2, { color: PALETTE.muted, children: [
1486
1475
  " \xB7 ",
1487
1476
  t.app.subtitle
1488
1477
  ] })
@@ -1490,22 +1479,22 @@ function MainMenu({ cfg }) {
1490
1479
  /* @__PURE__ */ jsx8(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
1491
1480
  const active = i === selected;
1492
1481
  const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(it.label)));
1493
- return /* @__PURE__ */ jsxs2(Box3, { children: [
1482
+ return /* @__PURE__ */ jsxs(Box3, { children: [
1494
1483
  /* @__PURE__ */ jsx8(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
1495
- /* @__PURE__ */ jsxs2(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
1484
+ /* @__PURE__ */ jsxs(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
1496
1485
  "[",
1497
1486
  it.key,
1498
1487
  "]"
1499
1488
  ] }),
1500
1489
  /* @__PURE__ */ jsx8(Text2, { children: " " }),
1501
- /* @__PURE__ */ jsxs2(Text2, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
1490
+ /* @__PURE__ */ jsxs(Text2, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
1502
1491
  it.label,
1503
1492
  pad
1504
1493
  ] }),
1505
1494
  /* @__PURE__ */ jsx8(Text2, { color: PALETTE.muted, children: it.hint })
1506
1495
  ] }, it.key);
1507
1496
  }) }),
1508
- /* @__PURE__ */ jsx8(Box3, { marginTop: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
1497
+ /* @__PURE__ */ jsx8(Box3, { marginTop: 2, children: /* @__PURE__ */ jsxs(Text2, { color: PALETTE.muted, children: [
1509
1498
  t.mainMenu.hint,
1510
1499
  " \xB7 ",
1511
1500
  t.mainMenu.helpHint
@@ -1876,6 +1865,12 @@ function sparkline(values) {
1876
1865
  return SPARK[Math.max(0, Math.min(SPARK.length - 1, idx))];
1877
1866
  }).join("");
1878
1867
  }
1868
+ function dailyValues(sessions, days, metric, now = /* @__PURE__ */ new Date()) {
1869
+ const buckets = dailyBuckets(sessions, days, now);
1870
+ if (metric === "wpm") return buckets.map((b) => b.wpm);
1871
+ if (metric === "accuracy") return buckets.map((b) => b.accuracy * 100);
1872
+ return buckets.map((b) => b.sessions);
1873
+ }
1879
1874
  function dailyBuckets(sessions, days, now = /* @__PURE__ */ new Date()) {
1880
1875
  const out = [];
1881
1876
  const byDay = /* @__PURE__ */ new Map();
@@ -1974,71 +1969,105 @@ function useSessionPersistence(meta) {
1974
1969
  }
1975
1970
 
1976
1971
  // src/ui/screens/StealthPracticeLayout.tsx
1977
- import { Box as Box4, Text as Text3 } from "ink";
1978
- import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
1972
+ import { Box as Box4, Text as Text3, useStdout as useStdout2 } from "ink";
1973
+ import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
1979
1974
  var TYPED = "#d4d4d4";
1980
1975
  var UNTYPED = "#808080";
1981
1976
  var DIM = "#6b6b6b";
1977
+ var RIGHT_WIDTH = 28;
1982
1978
  function fmtTime(ms) {
1983
1979
  const total = Math.floor(ms / 1e3);
1984
1980
  const m = Math.floor(total / 60);
1985
1981
  const s = total % 60;
1986
1982
  return `${m}:${String(s).padStart(2, "0")}`;
1987
1983
  }
1984
+ function useLeftWidth() {
1985
+ const { stdout } = useStdout2();
1986
+ const cols = stdout?.columns ?? 80;
1987
+ return Math.max(20, cols - RIGHT_WIDTH);
1988
+ }
1989
+ function Row({ left, right }) {
1990
+ const leftWidth = useLeftWidth();
1991
+ return /* @__PURE__ */ jsxs2(Box4, { children: [
1992
+ /* @__PURE__ */ jsx9(Box4, { width: leftWidth, children: left }),
1993
+ /* @__PURE__ */ jsx9(Box4, { width: RIGHT_WIDTH, justifyContent: "flex-end", children: right })
1994
+ ] });
1995
+ }
1988
1996
  function StealthTyping(props) {
1989
1997
  const t = useStrings();
1990
1998
  const target = [...props.target];
1991
1999
  const typed = [...props.typed];
1992
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: "100%", height: "100%", paddingX: 4, paddingY: 3, children: [
1993
- /* @__PURE__ */ jsx9(Box4, { flexGrow: 1 }),
1994
- /* @__PURE__ */ jsxs3(Box4, { children: [
1995
- /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "[" }),
1996
- target.map((ch, i) => {
1997
- const isTyped = i < typed.length;
1998
- const display = props.hideTarget && !isTyped ? "_" : isTyped ? typed[i] : ch;
1999
- const color = isTyped ? TYPED : UNTYPED;
2000
- return /* @__PURE__ */ jsx9(Text3, { color, inverse: props.error && isTyped && i === typed.length - 1, children: display }, i);
2001
- }),
2002
- /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "]" })
2003
- ] }),
2004
- /* @__PURE__ */ jsxs3(Box4, { children: [
2005
- props.phonetic && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.phonetic }),
2006
- props.phonetic && props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: " \xB7 " }),
2007
- props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.translation.slice(0, 1).join("") })
2008
- ] }),
2009
- /* @__PURE__ */ jsx9(Box4, { children: props.info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.infoFmt(
2010
- props.info.dictName,
2011
- props.info.chapterLabel,
2012
- props.info.completed,
2013
- props.info.total,
2014
- props.info.wpm,
2015
- props.info.accPct
2016
- ) }) : /* @__PURE__ */ jsx9(Text3, { children: " " }) }),
2017
- /* @__PURE__ */ jsx9(Box4, { flexGrow: 3 })
2000
+ const wordCell = /* @__PURE__ */ jsxs2(Box4, { children: [
2001
+ /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "[" }),
2002
+ target.map((ch, i) => {
2003
+ const isTyped = i < typed.length;
2004
+ const display = props.hideTarget && !isTyped ? "_" : isTyped ? typed[i] : ch;
2005
+ const color = isTyped ? TYPED : UNTYPED;
2006
+ return /* @__PURE__ */ jsx9(Text3, { color, inverse: props.error && isTyped && i === typed.length - 1, children: display }, i);
2007
+ }),
2008
+ /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "]" })
2009
+ ] });
2010
+ const phoneticTransCell = /* @__PURE__ */ jsxs2(Box4, { children: [
2011
+ props.phonetic && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.phonetic }),
2012
+ props.phonetic && props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: " \xB7 " }),
2013
+ props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.translation.slice(0, 1).join("") })
2014
+ ] });
2015
+ const info = props.info;
2016
+ const accFmt = Number.isInteger(info.accPct) ? `${info.accPct}` : info.accPct.toFixed(1);
2017
+ const right1 = info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: `${info.dictName} \xB7 ${info.chapterLabel}` }) : /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
2018
+ "Ctrl+I ",
2019
+ t.stealth.infoChipLabel
2020
+ ] });
2021
+ const right2 = info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: `${info.completed}/${info.total} \xB7 ${info.wpm}wpm \xB7 ${accFmt}%` }) : /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
2022
+ "Esc ",
2023
+ t.common.back
2024
+ ] });
2025
+ const right3 = info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: fmtTime(info.elapsedMs) }) : /* @__PURE__ */ jsx9(Text3, { children: " " });
2026
+ return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
2027
+ /* @__PURE__ */ jsx9(Row, { left: wordCell, right: right1 }),
2028
+ /* @__PURE__ */ jsx9(Row, { left: phoneticTransCell, right: right2 }),
2029
+ /* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: right3 })
2018
2030
  ] });
2019
2031
  }
2020
2032
  function StealthPaused() {
2021
2033
  const t = useStrings();
2022
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: "100%", height: "100%", paddingX: 4, paddingY: 3, children: [
2023
- /* @__PURE__ */ jsx9(Box4, { flexGrow: 1 }),
2024
- /* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: t.stealth.paused }) }),
2025
- /* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.resumeHint }) }),
2026
- /* @__PURE__ */ jsx9(Box4, { flexGrow: 3 })
2034
+ return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
2035
+ /* @__PURE__ */ jsx9(
2036
+ Row,
2037
+ {
2038
+ left: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: t.stealth.paused }),
2039
+ right: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.pausedHintRight })
2040
+ }
2041
+ ),
2042
+ /* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
2043
+ "Esc ",
2044
+ t.common.back
2045
+ ] }) }),
2046
+ /* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsx9(Text3, { children: " " }) })
2027
2047
  ] });
2028
2048
  }
2029
2049
  function StealthSummary(props) {
2030
2050
  const t = useStrings();
2031
- const line = `${t.stealth.chapterDone} \xB7 ${props.wordCount}w \xB7 ${props.wpm}wpm \xB7 ${props.accPct}% \xB7 ${fmtTime(props.durationMs)}`;
2032
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: "100%", height: "100%", paddingX: 4, paddingY: 3, children: [
2033
- /* @__PURE__ */ jsx9(Box4, { flexGrow: 1 }),
2034
- /* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: line }) }),
2035
- /* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.nextHint }) }),
2036
- /* @__PURE__ */ jsx9(Box4, { flexGrow: 3 })
2051
+ const accFmt = Number.isInteger(props.accPct) ? `${props.accPct}` : props.accPct.toFixed(1);
2052
+ const line = `${t.stealth.chapterDone} \xB7 ${props.wordCount}w \xB7 ${props.wpm}wpm \xB7 ${accFmt}% \xB7 ${fmtTime(props.durationMs)}`;
2053
+ return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
2054
+ /* @__PURE__ */ jsx9(
2055
+ Row,
2056
+ {
2057
+ left: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: line }),
2058
+ right: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.nextHintRight })
2059
+ }
2060
+ ),
2061
+ /* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
2062
+ "Esc ",
2063
+ t.common.back
2064
+ ] }) }),
2065
+ /* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsx9(Text3, { children: " " }) })
2037
2066
  ] });
2038
2067
  }
2039
2068
 
2040
2069
  // src/ui/screens/PracticeScreen.tsx
2041
- import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
2070
+ import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
2042
2071
  function PracticeScreen({ params }) {
2043
2072
  const { dictId, chapterIndex, mode } = params;
2044
2073
  const { cfg } = useAppState();
@@ -2129,6 +2158,12 @@ function PracticeRunner({
2129
2158
  const lastEffectRef = useRef3(null);
2130
2159
  const lastIndexRef = useRef3(-1);
2131
2160
  const [infoVisible, setInfoVisible] = useState8(false);
2161
+ const [infoShownAt, setInfoShownAt] = useState8(null);
2162
+ useEffect6(() => {
2163
+ if (infoShownAt === null) return;
2164
+ const id = setTimeout(() => setInfoVisible(false), 2e3);
2165
+ return () => clearTimeout(id);
2166
+ }, [infoShownAt]);
2132
2167
  const { session, lastEffect, tick } = useWordLoop({
2133
2168
  playlist: loaded.playlist,
2134
2169
  enabled: phase === "typing",
@@ -2173,7 +2208,8 @@ function PracticeRunner({
2173
2208
  useInput3(
2174
2209
  (input, key) => {
2175
2210
  if (key.ctrl && input === "i") {
2176
- setInfoVisible((v) => !v);
2211
+ setInfoVisible(true);
2212
+ setInfoShownAt(Date.now());
2177
2213
  return;
2178
2214
  }
2179
2215
  },
@@ -2274,7 +2310,8 @@ function PracticeRunner({
2274
2310
  completed,
2275
2311
  total: loaded.playlist.length,
2276
2312
  wpm,
2277
- accPct
2313
+ accPct,
2314
+ elapsedMs
2278
2315
  }
2279
2316
  }
2280
2317
  );
@@ -2342,7 +2379,7 @@ function fmtTime2(ms) {
2342
2379
  function TypingLayout(props) {
2343
2380
  const t = useStrings();
2344
2381
  const progressFrac = props.total === 0 ? 0 : props.completed / props.total;
2345
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2382
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2346
2383
  /* @__PURE__ */ jsx10(
2347
2384
  StatusBar,
2348
2385
  {
@@ -2356,7 +2393,7 @@ function TypingLayout(props) {
2356
2393
  elapsedMs: props.elapsedMs
2357
2394
  }
2358
2395
  ),
2359
- /* @__PURE__ */ jsxs4(Box5, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
2396
+ /* @__PURE__ */ jsxs3(Box5, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
2360
2397
  /* @__PURE__ */ jsx10(
2361
2398
  BigWord,
2362
2399
  {
@@ -2366,12 +2403,12 @@ function TypingLayout(props) {
2366
2403
  hideTarget: props.hideTarget
2367
2404
  }
2368
2405
  ),
2369
- props.phonetic && /* @__PURE__ */ jsx10(Box5, { marginTop: 3, children: /* @__PURE__ */ jsx10(Text4, { italic: true, color: PALETTE.muted, children: props.phonetic }) }),
2370
- props.translation.length > 0 && /* @__PURE__ */ jsx10(Box5, { marginTop: 2, flexDirection: "column", alignItems: "center", children: props.translation.slice(0, 2).map((tr, i) => /* @__PURE__ */ jsx10(Text4, { color: PALETTE.primary, children: tr }, i)) })
2406
+ props.phonetic && /* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { italic: true, color: PALETTE.muted, children: props.phonetic }) }),
2407
+ props.translation.length > 0 && /* @__PURE__ */ jsx10(Box5, { marginTop: 1, flexDirection: "column", alignItems: "center", children: props.translation.slice(0, 2).map((tr, i) => /* @__PURE__ */ jsx10(Text4, { color: PALETTE.primary, children: tr }, i)) })
2371
2408
  ] }),
2372
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
2409
+ /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
2373
2410
  /* @__PURE__ */ jsx10(ProgressBar, { frac: progressFrac }),
2374
- /* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
2411
+ /* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsxs3(Text4, { color: PALETTE.muted, children: [
2375
2412
  props.completed,
2376
2413
  "/",
2377
2414
  props.total,
@@ -2397,7 +2434,7 @@ function StatusBar(props) {
2397
2434
  const name = truncateName(props.dictName, 20);
2398
2435
  const left = props.mode === "review" ? `${name} \xB7 ${t.practice.reviewLabel} \xB7 ${accentName}` : `${name} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName} \xB7 ${accentName}`;
2399
2436
  const right = `${props.completed}/${props.total} \xB7 ${fmtTime2(props.elapsedMs)}`;
2400
- return /* @__PURE__ */ jsxs4(Box5, { children: [
2437
+ return /* @__PURE__ */ jsxs3(Box5, { children: [
2401
2438
  /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: left }),
2402
2439
  /* @__PURE__ */ jsx10(Box5, { flexGrow: 1 }),
2403
2440
  /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: right })
@@ -2408,7 +2445,7 @@ function ProgressBar({ frac }) {
2408
2445
  const width = Math.max(20, Math.min(72, cols - 16));
2409
2446
  const filled = Math.round(width * Math.max(0, Math.min(1, frac)));
2410
2447
  const empty = width - filled;
2411
- return /* @__PURE__ */ jsxs4(Box5, { justifyContent: "center", children: [
2448
+ return /* @__PURE__ */ jsxs3(Box5, { justifyContent: "center", children: [
2412
2449
  /* @__PURE__ */ jsx10(Text4, { color: PALETTE.accent, children: "\u2501".repeat(filled) }),
2413
2450
  /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: "\u2500".repeat(empty) })
2414
2451
  ] });
@@ -2417,7 +2454,7 @@ function PausedView(props) {
2417
2454
  const t = useStrings();
2418
2455
  const frac = props.total === 0 ? 0 : props.completed / props.total;
2419
2456
  const subtitle = props.mode === "review" ? `${truncateName(props.dictName, 20)} \xB7 ${t.practice.reviewLabel}` : `${truncateName(props.dictName, 20)} \xB7 ${t.practice.pause.chapter(props.chapterIndex + 1, props.totalChapters)}`;
2420
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2457
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2421
2458
  /* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.warning, children: t.practice.pause.title }),
2422
2459
  /* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: subtitle }) }),
2423
2460
  /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(ProgressBar, { frac }) }),
@@ -2427,9 +2464,9 @@ function PausedView(props) {
2427
2464
  }
2428
2465
  function ErrorView({ msg }) {
2429
2466
  const t = useStrings();
2430
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2467
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2431
2468
  /* @__PURE__ */ jsx10(Text4, { color: PALETTE.error, children: msg }),
2432
- /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
2469
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsxs3(Text4, { color: PALETTE.muted, children: [
2433
2470
  "Esc ",
2434
2471
  t.common.back
2435
2472
  ] }) }),
@@ -2459,10 +2496,10 @@ function SummaryView(props) {
2459
2496
  const subtitle = props.mode === "review" ? `${name} \xB7 ${t.practice.reviewLabel}` : `${name} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName}`;
2460
2497
  const nextLabel = props.mode === "loop" ? t.practice.summary.loopAgain : props.mode === "review" || props.chapterIndex + 1 >= props.totalChapters ? t.practice.summary.backMenu : t.practice.summary.nextChapter;
2461
2498
  const footer = `Enter ${nextLabel} \xB7 m ${t.practice.summary.reviewMistakes} \xB7 Esc ${t.practice.summary.backMenu}`;
2462
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 1, width: "100%", height: "100%", children: [
2499
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 1, width: "100%", height: "100%", children: [
2463
2500
  /* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.success, children: t.practice.chapterComplete }),
2464
2501
  /* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: subtitle }) }),
2465
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 3, flexDirection: "row", justifyContent: "center", children: [
2502
+ /* @__PURE__ */ jsxs3(Box5, { marginTop: 3, flexDirection: "row", justifyContent: "center", children: [
2466
2503
  /* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.words, value: String(summary.wordCount), color: PALETTE.text }),
2467
2504
  /* @__PURE__ */ jsx10(
2468
2505
  StatCard,
@@ -2481,7 +2518,7 @@ function SummaryView(props) {
2481
2518
  ] });
2482
2519
  }
2483
2520
  function StatCard({ label, value, color }) {
2484
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", marginX: 3, children: [
2521
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", marginX: 3, children: [
2485
2522
  /* @__PURE__ */ jsx10(Text4, { bold: true, color, children: value }),
2486
2523
  /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: label })
2487
2524
  ] });
@@ -2494,7 +2531,7 @@ import { Box as Box7, Text as Text6, useInput as useInput5, useStdout as useStdo
2494
2531
  // src/ui/components/ActionPanel.tsx
2495
2532
  import { useState as useState9 } from "react";
2496
2533
  import { Box as Box6, Text as Text5, useInput as useInput4 } from "ink";
2497
- import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
2534
+ import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
2498
2535
  function ActionPanel({ title, items, onClose }) {
2499
2536
  const enabledIndices = items.map((it, i) => it.disabled ? -1 : i).filter((i) => i >= 0);
2500
2537
  const initial = enabledIndices[0] ?? 0;
@@ -2534,7 +2571,7 @@ function ActionPanel({ title, items, onClose }) {
2534
2571
  });
2535
2572
  const maxLabel = Math.max(...items.map((it) => it.label.length));
2536
2573
  const width = Math.max(maxLabel + 8, title.length + 4, 24);
2537
- return /* @__PURE__ */ jsxs5(
2574
+ return /* @__PURE__ */ jsxs4(
2538
2575
  Box6,
2539
2576
  {
2540
2577
  flexDirection: "column",
@@ -2548,7 +2585,7 @@ function ActionPanel({ title, items, onClose }) {
2548
2585
  items.map((it, i) => {
2549
2586
  const active = i === selected;
2550
2587
  const color = it.disabled ? PALETTE.muted : active ? PALETTE.text : PALETTE.muted;
2551
- return /* @__PURE__ */ jsxs5(Box6, { children: [
2588
+ return /* @__PURE__ */ jsxs4(Box6, { children: [
2552
2589
  /* @__PURE__ */ jsx11(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
2553
2590
  /* @__PURE__ */ jsx11(Text5, { bold: active, color, children: it.label })
2554
2591
  ] }, i);
@@ -2560,7 +2597,7 @@ function ActionPanel({ title, items, onClose }) {
2560
2597
  }
2561
2598
 
2562
2599
  // src/ui/screens/DictBrowser.tsx
2563
- import { Fragment, jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
2600
+ import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
2564
2601
  function DictBrowser({ params }) {
2565
2602
  const nav = useNav();
2566
2603
  const { cfg, setCfg } = useAppState();
@@ -2734,22 +2771,22 @@ function DictBrowser({ params }) {
2734
2771
  }
2735
2772
  ) });
2736
2773
  }
2737
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2738
- /* @__PURE__ */ jsxs6(Box7, { children: [
2774
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2775
+ /* @__PURE__ */ jsxs5(Box7, { children: [
2739
2776
  /* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.accent, children: t.dict.title }),
2740
2777
  /* @__PURE__ */ jsx12(Box7, { flexGrow: 1 }),
2741
2778
  /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: filter ? `${t.dict.filterPlaceholder}: ${filter}_` : `${t.dict.filterPlaceholder}_` }),
2742
- /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
2779
+ /* @__PURE__ */ jsxs5(Text6, { color: PALETTE.muted, children: [
2743
2780
  " ",
2744
2781
  t.dict.entries(filtered.length)
2745
2782
  ] })
2746
2783
  ] }),
2747
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexGrow: 1, children: [
2784
+ /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexGrow: 1, children: [
2748
2785
  /* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "75%", paddingRight: 1, children: filtered.slice(start2, end).map((row, vi) => {
2749
2786
  const i = start2 + vi;
2750
2787
  const active = i === safeSelected;
2751
2788
  const isDefault = cfg.defaultDict === row.entry.id;
2752
- return /* @__PURE__ */ jsxs6(Box7, { children: [
2789
+ return /* @__PURE__ */ jsxs5(Box7, { children: [
2753
2790
  /* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }) }),
2754
2791
  /* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: row.local ? PALETTE.accent : PALETTE.muted, children: row.local ? "\u25CF" : "\u25CB" }) }),
2755
2792
  /* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: isDefault ? PALETTE.success : PALETTE.muted, children: isDefault ? "\u2605" : " " }) }),
@@ -2757,10 +2794,10 @@ function DictBrowser({ params }) {
2757
2794
  /* @__PURE__ */ jsx12(Box7, { width: 6, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: String(row.entry.length).padStart(5) }) })
2758
2795
  ] }, row.entry.id);
2759
2796
  }) }),
2760
- /* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "25%", paddingLeft: 1, children: current && /* @__PURE__ */ jsxs6(Fragment, { children: [
2797
+ /* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "25%", paddingLeft: 1, children: current && /* @__PURE__ */ jsxs5(Fragment, { children: [
2761
2798
  /* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.text, wrap: "wrap", children: current.entry.name }),
2762
2799
  /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: current.entry.id }),
2763
- /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, wrap: "wrap", children: [
2800
+ /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: PALETTE.muted, wrap: "wrap", children: [
2764
2801
  current.entry.language,
2765
2802
  " \xB7 ",
2766
2803
  current.entry.category
@@ -2772,10 +2809,10 @@ function DictBrowser({ params }) {
2772
2809
  cfg.defaultDict === current.entry.id && /* @__PURE__ */ jsx12(Box7, { children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.success, children: t.dict.defaultMark }) })
2773
2810
  ] }) })
2774
2811
  ] }),
2775
- pending && /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, children: [
2812
+ pending && /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, children: [
2776
2813
  pending.kind === "pulling" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.warning, children: t.dict.pulling(pending.id) }),
2777
2814
  pending.kind === "removing" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.warning, children: t.dict.removing(pending.id) }),
2778
- pending.kind === "refreshing" && /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.warning, children: [
2815
+ pending.kind === "refreshing" && /* @__PURE__ */ jsxs5(Text6, { color: PALETTE.warning, children: [
2779
2816
  t.dict.command.refreshList,
2780
2817
  "\u2026"
2781
2818
  ] }),
@@ -2788,7 +2825,7 @@ function DictBrowser({ params }) {
2788
2825
  // src/ui/screens/ConfigEditor.tsx
2789
2826
  import { useState as useState11 } from "react";
2790
2827
  import { Box as Box8, Text as Text7, useInput as useInput6 } from "ink";
2791
- import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
2828
+ import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
2792
2829
  var FIELDS = [
2793
2830
  { kind: "dictRef", path: "defaultDict", labelKey: "defaultDict" },
2794
2831
  { kind: "enum", path: "defaultMode", labelKey: "defaultMode", options: ["order", "dictation", "review", "random", "loop"] },
@@ -2901,7 +2938,7 @@ function ConfigEditor() {
2901
2938
  }
2902
2939
  });
2903
2940
  const labelW = Math.max(...FIELDS.map((f) => visibleWidth2(t.config.fields[f.labelKey]))) + 4;
2904
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2941
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2905
2942
  /* @__PURE__ */ jsx13(Text7, { bold: true, color: PALETTE.accent, children: t.config.title }),
2906
2943
  /* @__PURE__ */ jsx13(Box8, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: FIELDS.map((f, i) => {
2907
2944
  const active = i === selected;
@@ -2915,16 +2952,16 @@ function ConfigEditor() {
2915
2952
  );
2916
2953
  const label = t.config.fields[f.labelKey];
2917
2954
  const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
2918
- return /* @__PURE__ */ jsxs7(Box8, { children: [
2955
+ return /* @__PURE__ */ jsxs6(Box8, { children: [
2919
2956
  /* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
2920
- /* @__PURE__ */ jsxs7(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
2957
+ /* @__PURE__ */ jsxs6(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
2921
2958
  label,
2922
2959
  pad
2923
2960
  ] }),
2924
2961
  /* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: display })
2925
2962
  ] }, f.path);
2926
2963
  }) }),
2927
- error && /* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.error, children: [
2964
+ error && /* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: PALETTE.error, children: [
2928
2965
  "! ",
2929
2966
  error
2930
2967
  ] }) }),
@@ -2958,8 +2995,82 @@ function hintFor(field, editing, t) {
2958
2995
 
2959
2996
  // src/ui/screens/StatsViewer.tsx
2960
2997
  import { useEffect as useEffect8, useState as useState12 } from "react";
2961
- import { Box as Box9, Text as Text8, useInput as useInput7 } from "ink";
2962
- import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
2998
+ import { Box as Box10, Text as Text9, useInput as useInput7 } from "ink";
2999
+
3000
+ // src/ui/components/Heatmap.tsx
3001
+ import { Box as Box9, Text as Text8 } from "ink";
3002
+
3003
+ // src/util/heatmap.ts
3004
+ function bucketDays(values, days, todayWeekday) {
3005
+ if (days <= 0 || values.length !== days) {
3006
+ return Array.from({ length: 7 }, () => []);
3007
+ }
3008
+ const cols = Math.ceil((days - todayWeekday - 1) / 7) + 1;
3009
+ const grid = Array.from({ length: 7 }, () => Array(cols).fill(null));
3010
+ let col = cols - 1;
3011
+ let row = todayWeekday;
3012
+ for (let i = days - 1; i >= 0; i--) {
3013
+ grid[row][col] = values[i];
3014
+ if (row === 0) {
3015
+ row = 6;
3016
+ col -= 1;
3017
+ } else {
3018
+ row -= 1;
3019
+ }
3020
+ }
3021
+ return grid;
3022
+ }
3023
+ function intensity(value, max) {
3024
+ if (max <= 0 || value <= 0) return 0;
3025
+ const frac = value / max;
3026
+ if (frac <= 0.25) return 1;
3027
+ if (frac <= 0.5) return 2;
3028
+ if (frac <= 0.75) return 3;
3029
+ return 4;
3030
+ }
3031
+ function todayWeekdayMon0(now = /* @__PURE__ */ new Date()) {
3032
+ const d = now.getUTCDay();
3033
+ return d === 0 ? 6 : d - 1;
3034
+ }
3035
+
3036
+ // src/ui/components/Heatmap.tsx
3037
+ import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
3038
+ var COLORS = ["#1b1f23", "#0e4429", "#006d32", "#26a641", "#39d353"];
3039
+ var ROW_LABELS = ["M", "T", "W", "T", "F", "S", "S"];
3040
+ function Heatmap({ label, values, days, suffix = "" }) {
3041
+ const todayDow = todayWeekdayMon0();
3042
+ const grid = bucketDays(values, days, todayDow);
3043
+ const max = values.length === 0 ? 0 : Math.max(...values);
3044
+ const cols = grid[0]?.length ?? 0;
3045
+ const maxLabel = max > 0 ? `max ${Math.round(max)}${suffix}` : "";
3046
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", children: [
3047
+ /* @__PURE__ */ jsxs7(Box9, { children: [
3048
+ /* @__PURE__ */ jsx14(Box9, { width: 12, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: label }) }),
3049
+ /* @__PURE__ */ jsx14(Box9, { flexGrow: 1 }),
3050
+ maxLabel && /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: maxLabel })
3051
+ ] }),
3052
+ grid.map((row, r) => /* @__PURE__ */ jsxs7(Box9, { children: [
3053
+ /* @__PURE__ */ jsx14(Box9, { width: 3, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: ROW_LABELS[r] }) }),
3054
+ row.map((cell, c) => {
3055
+ if (cell === null) {
3056
+ return /* @__PURE__ */ jsxs7(Text8, { children: [
3057
+ " ",
3058
+ c < cols - 1 ? " " : ""
3059
+ ] }, c);
3060
+ }
3061
+ const lvl = intensity(cell, max);
3062
+ const sym = lvl === 0 ? "\xB7" : "\u25A0";
3063
+ return /* @__PURE__ */ jsxs7(Text8, { color: COLORS[lvl], children: [
3064
+ sym,
3065
+ c < cols - 1 ? " " : ""
3066
+ ] }, c);
3067
+ })
3068
+ ] }, r))
3069
+ ] });
3070
+ }
3071
+
3072
+ // src/ui/screens/StatsViewer.tsx
3073
+ import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
2963
3074
  var DAY_WINDOWS = [7, 14, 30, 90];
2964
3075
  function StatsViewer() {
2965
3076
  const nav = useNav();
@@ -2983,20 +3094,19 @@ function StatsViewer() {
2983
3094
  if (key.leftArrow) setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);
2984
3095
  });
2985
3096
  if (!sessions || !book) {
2986
- return /* @__PURE__ */ jsx14(Box9, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.loading }) });
3097
+ return /* @__PURE__ */ jsx15(Box10, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.loading }) });
2987
3098
  }
2988
3099
  if (sessions.length === 0) {
2989
- return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2990
- /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.none }),
2991
- /* @__PURE__ */ jsx14(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.nonePractice }) }),
2992
- /* @__PURE__ */ jsx14(Box9, { marginTop: 2, children: /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3100
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
3101
+ /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.none }),
3102
+ /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.nonePractice }) }),
3103
+ /* @__PURE__ */ jsx15(Box10, { marginTop: 2, children: /* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
2993
3104
  "Esc ",
2994
3105
  t.common.back
2995
3106
  ] }) })
2996
3107
  ] });
2997
3108
  }
2998
3109
  const days = DAY_WINDOWS[windowIdx];
2999
- const buckets = dailyBuckets(sessions, days);
3000
3110
  const streak = dailyStreak(sessions);
3001
3111
  const totalWords = sessions.reduce((a, s) => a + s.wordCount, 0);
3002
3112
  const totalErrors = sessions.reduce((a, s) => a + s.errors, 0);
@@ -3009,59 +3119,37 @@ function StatsViewer() {
3009
3119
  const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
3010
3120
  const recent = sessions.slice(-5).reverse();
3011
3121
  const top = topN(book, 8);
3012
- const wpms = buckets.map((b) => b.wpm);
3013
- const accs = buckets.map((b) => b.accuracy * 100);
3014
- const ses = buckets.map((b) => b.sessions);
3015
- return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
3016
- /* @__PURE__ */ jsx14(Text8, { bold: true, color: PALETTE.accent, children: t.stats.title }),
3017
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
3018
- /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.lifetime }),
3019
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, children: [
3020
- /* @__PURE__ */ jsx14(Stat, { label: t.stats.sessions, value: String(sessions.length) }),
3021
- /* @__PURE__ */ jsx14(Stat, { label: t.stats.words, value: String(totalWords) }),
3022
- /* @__PURE__ */ jsx14(Stat, { label: t.stats.errors, value: String(totalErrors) }),
3023
- /* @__PURE__ */ jsx14(Stat, { label: t.stats.wpm, value: String(overallWpm), accent: true }),
3024
- /* @__PURE__ */ jsx14(Stat, { label: t.stats.accuracy, value: `${Math.round(overallAcc * 1e3) / 10}%`, accent: true }),
3025
- /* @__PURE__ */ jsx14(Stat, { label: t.stats.streak, value: `${streak}d`, accent: true })
3122
+ const wpms = dailyValues(sessions, days, "wpm");
3123
+ const accs = dailyValues(sessions, days, "accuracy");
3124
+ const ses = dailyValues(sessions, days, "sessions");
3125
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
3126
+ /* @__PURE__ */ jsx15(Text9, { bold: true, color: PALETTE.accent, children: t.stats.title }),
3127
+ /* @__PURE__ */ jsxs8(Box10, { marginTop: 1, flexDirection: "column", children: [
3128
+ /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.lifetime }),
3129
+ /* @__PURE__ */ jsxs8(Box10, { marginTop: 1, children: [
3130
+ /* @__PURE__ */ jsx15(Stat, { label: t.stats.sessions, value: String(sessions.length) }),
3131
+ /* @__PURE__ */ jsx15(Stat, { label: t.stats.words, value: String(totalWords) }),
3132
+ /* @__PURE__ */ jsx15(Stat, { label: t.stats.errors, value: String(totalErrors) }),
3133
+ /* @__PURE__ */ jsx15(Stat, { label: t.stats.wpm, value: String(overallWpm), accent: true }),
3134
+ /* @__PURE__ */ jsx15(Stat, { label: t.stats.accuracy, value: `${Math.round(overallAcc * 1e3) / 10}%`, accent: true }),
3135
+ /* @__PURE__ */ jsx15(Stat, { label: t.stats.streak, value: `${streak}d`, accent: true })
3026
3136
  ] })
3027
3137
  ] }),
3028
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 2, flexDirection: "column", children: [
3029
- /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.last(days) }),
3030
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: PALETTE.muted, paddingX: 1, children: [
3031
- /* @__PURE__ */ jsx14(
3032
- SparkRow,
3033
- {
3034
- label: t.stats.wpm,
3035
- values: wpms,
3036
- maxLabel: t.stats.maxLabel
3037
- }
3038
- ),
3039
- /* @__PURE__ */ jsx14(
3040
- SparkRow,
3041
- {
3042
- label: t.stats.accuracy,
3043
- values: accs,
3044
- maxLabel: t.stats.maxLabel,
3045
- suffix: "%"
3046
- }
3047
- ),
3048
- /* @__PURE__ */ jsx14(
3049
- SparkRow,
3050
- {
3051
- label: t.stats.sessions,
3052
- values: ses,
3053
- maxLabel: t.stats.maxLabel
3054
- }
3055
- )
3138
+ /* @__PURE__ */ jsxs8(Box10, { marginTop: 2, flexDirection: "column", children: [
3139
+ /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.last(days) }),
3140
+ /* @__PURE__ */ jsxs8(Box10, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: PALETTE.muted, paddingX: 1, paddingY: 0, children: [
3141
+ /* @__PURE__ */ jsx15(Heatmap, { label: t.stats.wpm, values: wpms, days }),
3142
+ /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Heatmap, { label: t.stats.accuracy, values: accs, days, suffix: "%" }) }),
3143
+ /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Heatmap, { label: t.stats.sessions, values: ses, days }) })
3056
3144
  ] })
3057
3145
  ] }),
3058
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 2, flexDirection: "column", children: [
3059
- /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.recent }),
3060
- recent.map((s, i) => /* @__PURE__ */ jsx14(RecentRow, { session: s, units: t.stats.recentUnits }, i))
3146
+ /* @__PURE__ */ jsxs8(Box10, { marginTop: 2, flexDirection: "column", children: [
3147
+ /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.recent }),
3148
+ recent.map((s, i) => /* @__PURE__ */ jsx15(RecentRow, { session: s, units: t.stats.recentUnits }, i))
3061
3149
  ] }),
3062
- top.length > 0 && /* @__PURE__ */ jsxs8(Box9, { marginTop: 2, flexDirection: "column", children: [
3063
- /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.topMistakes }),
3064
- top.map(([word, entry]) => /* @__PURE__ */ jsx14(
3150
+ top.length > 0 && /* @__PURE__ */ jsxs8(Box10, { marginTop: 2, flexDirection: "column", children: [
3151
+ /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.topMistakes }),
3152
+ top.map(([word, entry]) => /* @__PURE__ */ jsx15(
3065
3153
  MistakeRow,
3066
3154
  {
3067
3155
  word,
@@ -3072,26 +3160,8 @@ function StatsViewer() {
3072
3160
  word
3073
3161
  ))
3074
3162
  ] }),
3075
- /* @__PURE__ */ jsx14(Box9, { flexGrow: 1 }),
3076
- /* @__PURE__ */ jsx14(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.footer }) })
3077
- ] });
3078
- }
3079
- function SparkRow({
3080
- label,
3081
- values,
3082
- maxLabel,
3083
- suffix = ""
3084
- }) {
3085
- const max = values.length === 0 ? 0 : Math.max(...values);
3086
- const maxDisplay = `${Math.round(max)}${suffix}`;
3087
- return /* @__PURE__ */ jsxs8(Box9, { children: [
3088
- /* @__PURE__ */ jsx14(Box9, { width: 10, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: label }) }),
3089
- /* @__PURE__ */ jsx14(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.accent, children: sparkline(values) }) }),
3090
- /* @__PURE__ */ jsx14(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3091
- maxLabel,
3092
- " ",
3093
- maxDisplay
3094
- ] }) })
3163
+ /* @__PURE__ */ jsx15(Box10, { flexGrow: 1 }),
3164
+ /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.footer }) })
3095
3165
  ] });
3096
3166
  }
3097
3167
  function RecentRow({
@@ -3100,14 +3170,14 @@ function RecentRow({
3100
3170
  }) {
3101
3171
  const name = useDictName(session.dictId);
3102
3172
  const display = truncateName(name, 14);
3103
- return /* @__PURE__ */ jsxs8(Box9, { children: [
3104
- /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3173
+ return /* @__PURE__ */ jsxs8(Box10, { children: [
3174
+ /* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
3105
3175
  " ",
3106
3176
  session.ts.replace("T", " ").slice(0, 16),
3107
3177
  " "
3108
3178
  ] }),
3109
- /* @__PURE__ */ jsx14(Text8, { color: PALETTE.text, children: display.padEnd(14) }),
3110
- /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3179
+ /* @__PURE__ */ jsx15(Text9, { color: PALETTE.text, children: display.padEnd(14) }),
3180
+ /* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
3111
3181
  " ",
3112
3182
  "ch",
3113
3183
  String(session.chapter + 1).padStart(3),
@@ -3137,34 +3207,34 @@ function MistakeRow({
3137
3207
  const firstId = dictIds[0] ?? "";
3138
3208
  const firstName = useDictName(firstId);
3139
3209
  const suffix = dictIds.length > 1 ? multiSuffix(dictIds.length - 1) : "";
3140
- return /* @__PURE__ */ jsxs8(Box9, { children: [
3141
- /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.error, children: [
3210
+ return /* @__PURE__ */ jsxs8(Box10, { children: [
3211
+ /* @__PURE__ */ jsxs8(Text9, { color: PALETTE.error, children: [
3142
3212
  " ",
3143
3213
  String(count).padStart(3),
3144
3214
  " "
3145
3215
  ] }),
3146
- /* @__PURE__ */ jsx14(Text8, { color: PALETTE.text, children: word.padEnd(20) }),
3147
- /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3216
+ /* @__PURE__ */ jsx15(Text9, { color: PALETTE.text, children: word.padEnd(20) }),
3217
+ /* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
3148
3218
  truncateName(firstName, 20),
3149
3219
  suffix
3150
3220
  ] })
3151
3221
  ] });
3152
3222
  }
3153
3223
  function Stat({ label, value, accent = false }) {
3154
- return /* @__PURE__ */ jsxs8(Box9, { marginRight: 3, children: [
3155
- /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3224
+ return /* @__PURE__ */ jsxs8(Box10, { marginRight: 3, children: [
3225
+ /* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
3156
3226
  label,
3157
3227
  " "
3158
3228
  ] }),
3159
- /* @__PURE__ */ jsx14(Text8, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
3229
+ /* @__PURE__ */ jsx15(Text9, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
3160
3230
  ] });
3161
3231
  }
3162
3232
 
3163
3233
  // src/ui/screens/WordLookup.tsx
3164
3234
  import { useEffect as useEffect9, useState as useState13 } from "react";
3165
- import { Box as Box10, Text as Text9, useInput as useInput8 } from "ink";
3235
+ import { Box as Box11, Text as Text10, useInput as useInput8 } from "ink";
3166
3236
  import { readdir } from "fs/promises";
3167
- import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
3237
+ import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
3168
3238
  async function listLocalDictIds() {
3169
3239
  try {
3170
3240
  const files = await readdir(paths.dictsDir);
@@ -3221,77 +3291,77 @@ function WordLookup() {
3221
3291
  }
3222
3292
  });
3223
3293
  if (loading) {
3224
- return /* @__PURE__ */ jsx15(Box10, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.indexing }) });
3294
+ return /* @__PURE__ */ jsx16(Box11, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.indexing }) });
3225
3295
  }
3226
3296
  if (allWords.length === 0) {
3227
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
3228
- /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.none }),
3229
- /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.pullFirst }) }),
3230
- /* @__PURE__ */ jsx15(Box10, { marginTop: 2, children: /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.muted, children: [
3297
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
3298
+ /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.none }),
3299
+ /* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.pullFirst }) }),
3300
+ /* @__PURE__ */ jsx16(Box11, { marginTop: 2, children: /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.muted, children: [
3231
3301
  "[Esc] ",
3232
3302
  t.common.back
3233
3303
  ] }) })
3234
3304
  ] });
3235
3305
  }
3236
3306
  const current = filtered[selected];
3237
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
3238
- /* @__PURE__ */ jsxs9(Box10, { children: [
3239
- /* @__PURE__ */ jsx15(Text9, { bold: true, color: PALETTE.accent, children: t.word.title }),
3240
- /* @__PURE__ */ jsx15(Box10, { flexGrow: 1 }),
3241
- /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.countAcross(allWords.length) })
3307
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
3308
+ /* @__PURE__ */ jsxs9(Box11, { children: [
3309
+ /* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.accent, children: t.word.title }),
3310
+ /* @__PURE__ */ jsx16(Box11, { flexGrow: 1 }),
3311
+ /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.countAcross(allWords.length) })
3242
3312
  ] }),
3243
- /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, children: [
3244
- /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: "> " }),
3245
- /* @__PURE__ */ jsx15(Text9, { color: PALETTE.text, children: query }),
3246
- /* @__PURE__ */ jsx15(Text9, { color: PALETTE.accent, children: "_" })
3313
+ /* @__PURE__ */ jsxs9(Box11, { marginTop: 1, children: [
3314
+ /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: "> " }),
3315
+ /* @__PURE__ */ jsx16(Text10, { color: PALETTE.text, children: query }),
3316
+ /* @__PURE__ */ jsx16(Text10, { color: PALETTE.accent, children: "_" })
3247
3317
  ] }),
3248
- /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexGrow: 1, children: [
3249
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", width: "40%", children: [
3250
- filtered.map((h, i) => /* @__PURE__ */ jsx15(HitRow, { hit: h, active: i === selected }, `${h.dictId}-${h.word.name}-${i}`)),
3251
- filtered.length === 0 && q && /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.noMatches(query) })
3318
+ /* @__PURE__ */ jsxs9(Box11, { marginTop: 1, flexGrow: 1, children: [
3319
+ /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", width: "40%", children: [
3320
+ filtered.map((h, i) => /* @__PURE__ */ jsx16(HitRow, { hit: h, active: i === selected }, `${h.dictId}-${h.word.name}-${i}`)),
3321
+ filtered.length === 0 && q && /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.noMatches(query) })
3252
3322
  ] }),
3253
- /* @__PURE__ */ jsx15(Box10, { flexDirection: "column", width: "60%", paddingLeft: 2, children: current && /* @__PURE__ */ jsx15(Detail, { hit: current, book }) })
3323
+ /* @__PURE__ */ jsx16(Box11, { flexDirection: "column", width: "60%", paddingLeft: 2, children: current && /* @__PURE__ */ jsx16(Detail, { hit: current, book }) })
3254
3324
  ] }),
3255
- /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.footer }) })
3325
+ /* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.footer }) })
3256
3326
  ] });
3257
3327
  }
3258
3328
  function HitRow({ hit, active }) {
3259
3329
  const name = useDictName(hit.dictId);
3260
- return /* @__PURE__ */ jsxs9(Box10, { children: [
3261
- /* @__PURE__ */ jsx15(Text9, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
3262
- /* @__PURE__ */ jsx15(Text9, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: hit.word.name.padEnd(20) }),
3263
- /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: truncateName(name, 18) })
3330
+ return /* @__PURE__ */ jsxs9(Box11, { children: [
3331
+ /* @__PURE__ */ jsx16(Text10, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
3332
+ /* @__PURE__ */ jsx16(Text10, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: hit.word.name.padEnd(20) }),
3333
+ /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: truncateName(name, 18) })
3264
3334
  ] });
3265
3335
  }
3266
3336
  function Detail({ hit, book }) {
3267
3337
  const t = useStrings();
3268
3338
  const name = useDictName(hit.dictId);
3269
3339
  return /* @__PURE__ */ jsxs9(Fragment2, { children: [
3270
- /* @__PURE__ */ jsx15(Text9, { bold: true, color: PALETTE.text, children: hit.word.name }),
3271
- /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, children: [
3272
- hit.word.usphone && /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.muted, children: [
3340
+ /* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.text, children: hit.word.name }),
3341
+ /* @__PURE__ */ jsxs9(Box11, { marginTop: 1, children: [
3342
+ hit.word.usphone && /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.muted, children: [
3273
3343
  "US /",
3274
3344
  hit.word.usphone,
3275
3345
  "/ "
3276
3346
  ] }),
3277
- hit.word.ukphone && /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.muted, children: [
3347
+ hit.word.ukphone && /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.muted, children: [
3278
3348
  "UK /",
3279
3349
  hit.word.ukphone,
3280
3350
  "/"
3281
3351
  ] })
3282
3352
  ] }),
3283
- /* @__PURE__ */ jsx15(Box10, { marginTop: 1, flexDirection: "column", children: (hit.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.primary, children: [
3353
+ /* @__PURE__ */ jsx16(Box11, { marginTop: 1, flexDirection: "column", children: (hit.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.primary, children: [
3284
3354
  "\xB7 ",
3285
3355
  tr
3286
3356
  ] }, i)) }),
3287
- /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.inDict(truncateName(name, 22)) }) }),
3288
- book[hit.word.name] && /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.error, children: t.word.mistakes(book[hit.word.name].count, book[hit.word.name].lastSeen.slice(0, 10)) }) })
3357
+ /* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.inDict(truncateName(name, 22)) }) }),
3358
+ book[hit.word.name] && /* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.error, children: t.word.mistakes(book[hit.word.name].count, book[hit.word.name].lastSeen.slice(0, 10)) }) })
3289
3359
  ] });
3290
3360
  }
3291
3361
 
3292
3362
  // src/ui/screens/HelpScreen.tsx
3293
- import { Box as Box11, Text as Text10, useInput as useInput9 } from "ink";
3294
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
3363
+ import { Box as Box12, Text as Text11, useInput as useInput9 } from "ink";
3364
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
3295
3365
  function HelpScreen() {
3296
3366
  const nav = useNav();
3297
3367
  const t = useStrings();
@@ -3314,31 +3384,35 @@ function HelpScreen() {
3314
3384
  { title: t.help.sections.stats, keys: [k.cycleWindow, k.backMenu] },
3315
3385
  { title: t.help.sections.word, keys: [k.filter, k.navigate, k.backMenu] }
3316
3386
  ];
3317
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
3318
- /* @__PURE__ */ jsxs10(Box11, { children: [
3319
- /* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.accent, children: t.help.title }),
3320
- /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: " \xB7 " }),
3321
- /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.help.subtitle })
3387
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
3388
+ /* @__PURE__ */ jsxs10(Box12, { children: [
3389
+ /* @__PURE__ */ jsx17(Text11, { bold: true, color: PALETTE.accent, children: t.help.title }),
3390
+ /* @__PURE__ */ jsx17(Text11, { color: PALETTE.muted, children: " \xB7 " }),
3391
+ /* @__PURE__ */ jsx17(Text11, { color: PALETTE.muted, children: t.help.subtitle })
3322
3392
  ] }),
3323
- /* @__PURE__ */ jsx16(Box11, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: sections.map((sec) => /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
3324
- /* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.text, children: sec.title }),
3325
- sec.keys.map((line, i) => /* @__PURE__ */ jsx16(Box11, { children: /* @__PURE__ */ jsxs10(Text10, { color: PALETTE.muted, children: [
3393
+ /* @__PURE__ */ jsx17(Box12, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: sections.map((sec) => /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", marginTop: 1, children: [
3394
+ /* @__PURE__ */ jsx17(Text11, { bold: true, color: PALETTE.text, children: sec.title }),
3395
+ sec.keys.map((line, i) => /* @__PURE__ */ jsx17(Box12, { children: /* @__PURE__ */ jsxs10(Text11, { color: PALETTE.muted, children: [
3326
3396
  " \xB7 ",
3327
3397
  line
3328
3398
  ] }) }, i))
3329
3399
  ] }, sec.title)) }),
3330
- /* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.help.footer }) })
3400
+ /* @__PURE__ */ jsx17(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx17(Text11, { color: PALETTE.muted, children: t.help.footer }) })
3331
3401
  ] });
3332
3402
  }
3333
3403
 
3334
3404
  // src/ui/App.tsx
3335
- import { jsx as jsx17 } from "react/jsx-runtime";
3336
- function App({ initial, initialCfg }) {
3337
- return /* @__PURE__ */ jsx17(AppStateProvider, { initialCfg, children: /* @__PURE__ */ jsx17(LangBridge, { children: /* @__PURE__ */ jsx17(RegistryProvider, { children: /* @__PURE__ */ jsx17(AudioStatusProvider, { disabled: !initialCfg.sounds.master, children: /* @__PURE__ */ jsx17(NavProvider, { initial, children: /* @__PURE__ */ jsx17(Fullscreen, { children: /* @__PURE__ */ jsx17(Router, {}) }) }) }) }) }) });
3405
+ import { jsx as jsx18 } from "react/jsx-runtime";
3406
+ function App({
3407
+ initial,
3408
+ initialCfg,
3409
+ inline = false
3410
+ }) {
3411
+ return /* @__PURE__ */ jsx18(AppStateProvider, { initialCfg, children: /* @__PURE__ */ jsx18(LangBridge, { children: /* @__PURE__ */ jsx18(RegistryProvider, { children: /* @__PURE__ */ jsx18(AudioStatusProvider, { disabled: !initialCfg.sounds.master, children: /* @__PURE__ */ jsx18(NavProvider, { initial, children: inline ? /* @__PURE__ */ jsx18(Router, { inline: true }) : /* @__PURE__ */ jsx18(Fullscreen, { children: /* @__PURE__ */ jsx18(Router, {}) }) }) }) }) }) });
3338
3412
  }
3339
3413
  function LangBridge({ children }) {
3340
3414
  const { cfg } = useAppState();
3341
- return /* @__PURE__ */ jsx17(StringsProvider, { pref: cfg.language, children });
3415
+ return /* @__PURE__ */ jsx18(StringsProvider, { pref: cfg.language, children });
3342
3416
  }
3343
3417
  function screenKey(frame) {
3344
3418
  if (frame.name === "practice") {
@@ -3347,7 +3421,7 @@ function screenKey(frame) {
3347
3421
  }
3348
3422
  return frame.name;
3349
3423
  }
3350
- function Router() {
3424
+ function Router({ inline = false }) {
3351
3425
  const nav = useNav();
3352
3426
  const { cfg } = useAppState();
3353
3427
  const { exit } = useApp4();
@@ -3358,24 +3432,24 @@ function Router() {
3358
3432
  const frame = nav.current;
3359
3433
  const key = screenKey(frame);
3360
3434
  if (lastKeyRef.current !== key) {
3361
- if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[H");
3435
+ if (!inline && process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[H");
3362
3436
  lastKeyRef.current = key;
3363
3437
  }
3364
3438
  switch (frame.name) {
3365
3439
  case "main":
3366
- return /* @__PURE__ */ jsx17(MainMenu, { cfg });
3440
+ return /* @__PURE__ */ jsx18(MainMenu, { cfg });
3367
3441
  case "practice":
3368
- return /* @__PURE__ */ jsx17(PracticeScreen, { params: frame.params });
3442
+ return /* @__PURE__ */ jsx18(PracticeScreen, { params: frame.params });
3369
3443
  case "dict":
3370
- return /* @__PURE__ */ jsx17(DictBrowser, { params: frame.params });
3444
+ return /* @__PURE__ */ jsx18(DictBrowser, { params: frame.params });
3371
3445
  case "config":
3372
- return /* @__PURE__ */ jsx17(ConfigEditor, {});
3446
+ return /* @__PURE__ */ jsx18(ConfigEditor, {});
3373
3447
  case "stats":
3374
- return /* @__PURE__ */ jsx17(StatsViewer, {});
3448
+ return /* @__PURE__ */ jsx18(StatsViewer, {});
3375
3449
  case "word":
3376
- return /* @__PURE__ */ jsx17(WordLookup, {});
3450
+ return /* @__PURE__ */ jsx18(WordLookup, {});
3377
3451
  case "help":
3378
- return /* @__PURE__ */ jsx17(HelpScreen, {});
3452
+ return /* @__PURE__ */ jsx18(HelpScreen, {});
3379
3453
  }
3380
3454
  }
3381
3455
 
@@ -3468,12 +3542,17 @@ async function runPractice(dictIdArg, options) {
3468
3542
  const { waitUntilExit } = render(
3469
3543
  createElement(App, {
3470
3544
  initial: { name: "practice", params: { dictId, chapterIndex, mode, stealth } },
3471
- initialCfg: cfg
3545
+ initialCfg: cfg,
3546
+ inline: stealth
3472
3547
  }),
3473
3548
  { patchConsole: false, exitOnCtrlC: false }
3474
3549
  );
3475
3550
  await waitUntilExit();
3476
- ensureMainScreen();
3551
+ if (stealth && process.stdout.isTTY) {
3552
+ process.stdout.write("\x1B[3F\x1B[0J");
3553
+ } else {
3554
+ ensureMainScreen();
3555
+ }
3477
3556
  const { lang, t } = pickStrings(cfg.language);
3478
3557
  printSessionReport(report(), t, lang);
3479
3558
  }