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

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.
Files changed (3) hide show
  1. package/dist/cli.js +1167 -460
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // src/cli.ts
2
- import { Command as Command6 } from "commander";
2
+ import { Command as Command7 } from "commander";
3
3
 
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "qwerty-cli",
7
- version: "0.0.1-alpha.6",
7
+ version: "0.0.1-alpha.7",
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: {
@@ -150,7 +150,8 @@ var ConfigSchema = z.object({
150
150
  autoplayPronunciation: z.boolean().default(true),
151
151
  defaultMode: z.enum(["order", "dictation", "review", "random", "loop"]).default("order"),
152
152
  defaultDict: z.string().optional(),
153
- language: z.enum(["auto", "zh", "en"]).default("auto")
153
+ language: z.enum(["auto", "zh", "en"]).default("auto"),
154
+ stealth: z.enum(["off", "menu", "default"]).default("off")
154
155
  });
155
156
  var DEFAULTS = ConfigSchema.parse({});
156
157
  async function loadConfig() {
@@ -512,7 +513,7 @@ import { createElement } from "react";
512
513
 
513
514
  // src/ui/App.tsx
514
515
  import { useRef as useRef4 } from "react";
515
- import { useApp as useApp4, useInput as useInput8 } from "ink";
516
+ import { useApp as useApp4, useInput as useInput10 } from "ink";
516
517
 
517
518
  // src/ui/nav.tsx
518
519
  import { createContext, useContext, useState, useCallback } from "react";
@@ -804,12 +805,13 @@ var en = {
804
805
  back: "back",
805
806
  quit: "quit",
806
807
  on: "on",
807
- off: "off"
808
+ off: "off",
809
+ cancel: "cancel"
808
810
  },
809
811
  mainMenu: {
810
812
  items: {
811
813
  practiceLabel: "Practice",
812
- practiceHintWith: (id) => `start ${id}`,
814
+ practiceHintWith: (name) => `start ${name}`,
813
815
  practiceHintNone: "pick a dictionary",
814
816
  dictLabel: "Dictionaries",
815
817
  dictHint: "browse, pull, set default",
@@ -819,18 +821,19 @@ var en = {
819
821
  statsHint: "history & trends",
820
822
  configLabel: "Config",
821
823
  configHint: "edit preferences",
824
+ stealthLabel: "Stealth",
825
+ stealthHint: "quiet practice mode",
822
826
  quitLabel: "Quit",
823
- quitHint: "Ctrl+C also exits"
827
+ quitHint: "Esc or Ctrl+C also exits"
824
828
  },
825
- defaultDict: (id) => `default dict: ${id}`,
826
- noDefault: "default dict: (none \u2014 pick one in Dictionaries)",
827
- hint: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump"
829
+ hint: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump",
830
+ helpHint: "? help"
828
831
  },
829
832
  dict: {
830
833
  title: "Dictionaries",
831
834
  loading: "loading dictionaries\u2026",
832
835
  entries: (n) => `${n} entries`,
833
- filterPrompt: (q) => `/ ${q}`,
836
+ filterPlaceholder: "type to filter",
834
837
  local: "local \u2713",
835
838
  notLocal: "not local",
836
839
  defaultMark: "default \u2605",
@@ -839,7 +842,19 @@ var en = {
839
842
  pulling: (id) => `pulling ${id}\u2026`,
840
843
  removing: (id) => `removing ${id}\u2026`,
841
844
  errorOn: (id, msg) => `error on ${id}: ${msg}`,
842
- footer: "\u2191/\u2193 select \xB7 Enter set default \xB7 p practice \xB7 u pull \xB7 r remove \xB7 / filter \xB7 Esc back"
845
+ footer: "\u2191/\u2193 select \xB7 Enter actions \xB7 Ctrl+K more \xB7 Esc back",
846
+ action: {
847
+ title: "current dictionary",
848
+ setDefault: "set as default",
849
+ practice: "practice now",
850
+ delete: "delete local"
851
+ },
852
+ command: {
853
+ title: "more actions",
854
+ pull: "pull selected",
855
+ import: "import .json",
856
+ refreshList: "update dictionary list"
857
+ }
843
858
  },
844
859
  config: {
845
860
  title: "Config",
@@ -847,14 +862,18 @@ var en = {
847
862
  defaultDict: "default dict",
848
863
  defaultMode: "default mode",
849
864
  accent: "accent",
850
- mirror: "mirror",
865
+ mirror: "dict mirror",
851
866
  chapterSize: "chapter size",
852
867
  autoplayPronunciation: "autoplay pronunciation",
853
868
  soundsMaster: "sounds master",
854
869
  soundsKeystroke: "sounds keystroke",
855
870
  soundsFeedback: "sounds feedback",
856
871
  soundsKeySound: "sounds key sound",
857
- language: "language"
872
+ language: "language",
873
+ stealth: "stealth mode"
874
+ },
875
+ enumValues: {
876
+ stealth: { off: "off", menu: "show in menu", default: "default practice" }
858
877
  },
859
878
  hints: {
860
879
  editing: "type to edit \xB7 Enter save \xB7 Esc cancel",
@@ -865,7 +884,7 @@ var en = {
865
884
  }
866
885
  },
867
886
  stats: {
868
- title: "Stats",
887
+ title: "Stats \xB7 overview",
869
888
  loading: "loading stats\u2026",
870
889
  none: "No practice history yet.",
871
890
  nonePractice: "Run a practice session first.",
@@ -876,11 +895,14 @@ var en = {
876
895
  wpm: "wpm",
877
896
  accuracy: "accuracy",
878
897
  streak: "streak",
879
- last: (n) => `last ${n} days (n / N to cycle window)`,
880
- cycleWindow: "n / N cycle window \xB7 q back",
898
+ last: (n) => `last ${n} days (\u2190/\u2192 cycle window)`,
899
+ cycleWindow: "\u2190/\u2192 cycle window \xB7 Esc back",
881
900
  recent: "recent sessions",
882
901
  topMistakes: "top mistakes",
883
- footer: "n / N cycle window \xB7 q back"
902
+ footer: "\u2190/\u2192 cycle window \xB7 Esc back",
903
+ maxLabel: "max",
904
+ recentUnits: { words: "w", errors: "err", wpm: "wpm" },
905
+ multiDictSuffix: (n) => ` +${n} more`
884
906
  },
885
907
  word: {
886
908
  title: "Word lookup",
@@ -889,7 +911,7 @@ var en = {
889
911
  pullFirst: "Pull one in Dictionaries first.",
890
912
  countAcross: (n) => `${n} words across local dicts`,
891
913
  noMatches: (q) => `no matches for "${q}"`,
892
- inDict: (id) => `in: ${id}`,
914
+ inDict: (name) => `in: ${name}`,
893
915
  mistakes: (n, date) => `mistakes: ${n} (last ${date})`,
894
916
  footer: "type to filter \xB7 \u2191/\u2193 select \xB7 Esc back"
895
917
  },
@@ -921,10 +943,20 @@ var en = {
921
943
  accuracy: "accuracy",
922
944
  elapsed: (t) => `elapsed ${t}`
923
945
  },
946
+ pause: {
947
+ title: "PAUSED",
948
+ chapter: (c, t) => `chapter ${c}/${t}`,
949
+ progress: (completed, total) => `${completed}/${total}`,
950
+ hint: "Enter resume \xB7 Esc back to menu"
951
+ },
952
+ summary: {
953
+ loopAgain: "again",
954
+ nextChapter: "next chapter",
955
+ reviewMistakes: "review mistakes",
956
+ backMenu: "back to menu"
957
+ },
924
958
  footers: {
925
- typing: "Ctrl+N skip \xB7 Esc pause \xB7 Tab replay \xB7 Ctrl+C quit",
926
- paused: "[r] resume \xB7 [q] quit to menu",
927
- summary: "[n] next chapter \xB7 [m] review mistakes \xB7 [q] back to menu"
959
+ typing: "Ctrl+N skip \xB7 Esc pause \xB7 Tab replay"
928
960
  },
929
961
  errors: {
930
962
  noMistakes: "No mistakes to review yet. Practice some chapters first.",
@@ -944,7 +976,49 @@ var en = {
944
976
  accuracy: "accuracy",
945
977
  wpm: "wpm",
946
978
  newMistakes: "new mistakes",
947
- farewell: "see you next time."
979
+ farewell: "see you next time.",
980
+ notPracticed: "no practice this run"
981
+ },
982
+ help: {
983
+ title: "Help",
984
+ subtitle: "all shortcuts",
985
+ sections: {
986
+ main: "main menu",
987
+ practice: "practice",
988
+ dict: "dictionaries",
989
+ config: "config",
990
+ stats: "stats",
991
+ word: "word lookup",
992
+ global: "global"
993
+ },
994
+ keys: {
995
+ navigate: "\u2191/\u2193 navigate items",
996
+ select: "Enter confirm / continue",
997
+ letterJump: "letter jump to menu item",
998
+ pause: "Esc pause practice",
999
+ skip: "Ctrl+N skip current word (neutral)",
1000
+ replay: "Tab replay pronunciation",
1001
+ resume: "Enter resume from pause",
1002
+ backMenu: "Esc back to previous screen",
1003
+ backScreen: "Esc close panel or back",
1004
+ nextChapter: "Enter next chapter",
1005
+ reviewMistakes: "m review mistakes",
1006
+ filter: "type to filter list",
1007
+ itemActions: "Enter open actions panel",
1008
+ moreActions: "Ctrl+K more actions panel",
1009
+ cycleWindow: "\u2190/\u2192 cycle day window",
1010
+ stealthToggle: "Ctrl+I toggle stealth info row",
1011
+ helpScreen: "? open this help screen",
1012
+ quit: "Ctrl+C quit immediately"
1013
+ },
1014
+ footer: "Esc back"
1015
+ },
1016
+ stealth: {
1017
+ paused: "paused",
1018
+ chapterDone: "chapter done",
1019
+ resumeHint: "Enter resume \xB7 Esc menu",
1020
+ nextHint: "Enter next \xB7 Esc menu",
1021
+ infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
948
1022
  }
949
1023
  };
950
1024
  var zh = {
@@ -956,12 +1030,13 @@ var zh = {
956
1030
  back: "\u8FD4\u56DE",
957
1031
  quit: "\u9000\u51FA",
958
1032
  on: "\u5F00",
959
- off: "\u5173"
1033
+ off: "\u5173",
1034
+ cancel: "\u53D6\u6D88"
960
1035
  },
961
1036
  mainMenu: {
962
1037
  items: {
963
1038
  practiceLabel: "\u7EC3\u4E60",
964
- practiceHintWith: (id) => `\u5F00\u59CB ${id}`,
1039
+ practiceHintWith: (name) => `\u5F00\u59CB ${name}`,
965
1040
  practiceHintNone: "\u8BF7\u5148\u9009\u8BCD\u5178",
966
1041
  dictLabel: "\u8BCD\u5178",
967
1042
  dictHint: "\u6D4F\u89C8\u3001\u4E0B\u8F7D\u3001\u8BBE\u4E3A\u9ED8\u8BA4",
@@ -971,18 +1046,19 @@ var zh = {
971
1046
  statsHint: "\u5386\u53F2\u4E0E\u8D8B\u52BF",
972
1047
  configLabel: "\u8BBE\u7F6E",
973
1048
  configHint: "\u4FEE\u6539\u504F\u597D",
1049
+ stealthLabel: "\u6478\u9C7C",
1050
+ stealthHint: "\u5B89\u9759\u7EC3\u4E60\u6A21\u5F0F",
974
1051
  quitLabel: "\u9000\u51FA",
975
- quitHint: "Ctrl+C \u4EA6\u53EF\u9000\u51FA"
1052
+ quitHint: "Esc \u6216 Ctrl+C \u9000\u51FA"
976
1053
  },
977
- defaultDict: (id) => `\u9ED8\u8BA4\u8BCD\u5178:${id}`,
978
- noDefault: "\u9ED8\u8BA4\u8BCD\u5178:(\u672A\u8BBE\u7F6E \u2014 \u5728\u300C\u8BCD\u5178\u300D\u4E2D\u9009\u62E9)",
979
- hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE"
1054
+ hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE",
1055
+ helpHint: "? \u5E2E\u52A9"
980
1056
  },
981
1057
  dict: {
982
1058
  title: "\u8BCD\u5178",
983
1059
  loading: "\u52A0\u8F7D\u8BCD\u5178\u4E2D\u2026",
984
1060
  entries: (n) => `${n} \u90E8\u8BCD\u5178`,
985
- filterPrompt: (q) => `/ ${q}`,
1061
+ filterPlaceholder: "\u8F93\u5165\u8FC7\u6EE4",
986
1062
  local: "\u5DF2\u4E0B\u8F7D \u2713",
987
1063
  notLocal: "\u672A\u4E0B\u8F7D",
988
1064
  defaultMark: "\u9ED8\u8BA4 \u2605",
@@ -991,7 +1067,19 @@ var zh = {
991
1067
  pulling: (id) => `\u62C9\u53D6 ${id} \u4E2D\u2026`,
992
1068
  removing: (id) => `\u5220\u9664 ${id} \u4E2D\u2026`,
993
1069
  errorOn: (id, msg) => `${id} \u51FA\u9519:${msg}`,
994
- footer: "\u2191/\u2193 \u9009\u62E9 \xB7 Enter \u8BBE\u4E3A\u9ED8\u8BA4 \xB7 p \u5F00\u59CB\u7EC3\u4E60 \xB7 u \u62C9\u53D6 \xB7 r \u5220\u9664 \xB7 / \u8FC7\u6EE4 \xB7 Esc \u8FD4\u56DE"
1070
+ footer: "\u2191/\u2193 \u9009\u62E9 \xB7 Enter \u64CD\u4F5C \xB7 Ctrl+K \u66F4\u591A \xB7 Esc \u8FD4\u56DE",
1071
+ action: {
1072
+ title: "\u5F53\u524D\u8BCD\u5178",
1073
+ setDefault: "\u8BBE\u4E3A\u9ED8\u8BA4",
1074
+ practice: "\u7ACB\u5373\u7EC3\u4E60",
1075
+ delete: "\u5220\u9664\u672C\u5730"
1076
+ },
1077
+ command: {
1078
+ title: "\u66F4\u591A\u529F\u80FD",
1079
+ pull: "\u62C9\u53D6\u9009\u4E2D",
1080
+ import: "\u5BFC\u5165 .json",
1081
+ refreshList: "\u66F4\u65B0\u8BCD\u5178\u5217\u8868"
1082
+ }
995
1083
  },
996
1084
  config: {
997
1085
  title: "\u8BBE\u7F6E",
@@ -999,14 +1087,18 @@ var zh = {
999
1087
  defaultDict: "\u9ED8\u8BA4\u8BCD\u5178",
1000
1088
  defaultMode: "\u9ED8\u8BA4\u6A21\u5F0F",
1001
1089
  accent: "\u53D1\u97F3",
1002
- mirror: "\u955C\u50CF\u6E90",
1090
+ mirror: "\u8BCD\u5178\u955C\u50CF\u6E90",
1003
1091
  chapterSize: "\u7AE0\u8282\u5355\u8BCD\u6570",
1004
1092
  autoplayPronunciation: "\u81EA\u52A8\u64AD\u653E\u53D1\u97F3",
1005
1093
  soundsMaster: "\u97F3\u6548\u603B\u5F00\u5173",
1006
1094
  soundsKeystroke: "\u6309\u952E\u97F3",
1007
1095
  soundsFeedback: "\u53CD\u9988\u97F3",
1008
1096
  soundsKeySound: "\u6309\u952E\u97F3\u8272",
1009
- language: "\u8BED\u8A00"
1097
+ language: "\u8BED\u8A00",
1098
+ stealth: "\u6478\u9C7C\u6A21\u5F0F"
1099
+ },
1100
+ enumValues: {
1101
+ stealth: { off: "\u5173\u95ED", menu: "\u4E3B\u83DC\u5355\u663E\u793A", default: "\u9ED8\u8BA4\u7EC3\u4E60\u6A21\u5F0F" }
1010
1102
  },
1011
1103
  hints: {
1012
1104
  editing: "\u8F93\u5165\u4FEE\u6539 \xB7 Enter \u4FDD\u5B58 \xB7 Esc \u53D6\u6D88",
@@ -1017,7 +1109,7 @@ var zh = {
1017
1109
  }
1018
1110
  },
1019
1111
  stats: {
1020
- title: "\u7EDF\u8BA1",
1112
+ title: "\u7EDF\u8BA1 \xB7 \u6982\u89C8",
1021
1113
  loading: "\u52A0\u8F7D\u7EDF\u8BA1\u4E2D\u2026",
1022
1114
  none: "\u8FD8\u6CA1\u6709\u7EC3\u4E60\u8BB0\u5F55\u3002",
1023
1115
  nonePractice: "\u5148\u6765\u4E00\u6B21\u7EC3\u4E60\u5427\u3002",
@@ -1028,11 +1120,14 @@ var zh = {
1028
1120
  wpm: "\u901F\u5EA6",
1029
1121
  accuracy: "\u51C6\u786E\u7387",
1030
1122
  streak: "\u8FDE\u7EED\u5929\u6570",
1031
- last: (n) => `\u6700\u8FD1 ${n} \u5929 (n / N \u5207\u6362\u7A97\u53E3)`,
1032
- cycleWindow: "n / N \u5207\u6362\u7A97\u53E3 \xB7 q \u8FD4\u56DE",
1123
+ last: (n) => `\u6700\u8FD1 ${n} \u5929 (\u2190/\u2192 \u5207\u6362\u7A97\u53E3)`,
1124
+ cycleWindow: "\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",
1033
1125
  recent: "\u6700\u8FD1\u4F1A\u8BDD",
1034
1126
  topMistakes: "\u9AD8\u9891\u9519\u8BCD",
1035
- footer: "n / N \u5207\u6362\u7A97\u53E3 \xB7 q \u8FD4\u56DE"
1127
+ footer: "\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",
1128
+ maxLabel: "\u6700\u5927",
1129
+ recentUnits: { words: "\u8BCD", errors: "\u9519", wpm: "\u901F" },
1130
+ multiDictSuffix: (n) => ` \u7B49 ${n} \u90E8`
1036
1131
  },
1037
1132
  word: {
1038
1133
  title: "\u67E5\u8BCD",
@@ -1041,7 +1136,7 @@ var zh = {
1041
1136
  pullFirst: "\u5148\u5728\u300C\u8BCD\u5178\u300D\u4E2D\u62C9\u53D6\u4E00\u90E8\u3002",
1042
1137
  countAcross: (n) => `\u672C\u5730\u8BCD\u5178\u5171 ${n} \u8BCD`,
1043
1138
  noMatches: (q) => `\u6CA1\u6709\u5339\u914D\u300C${q}\u300D\u7684\u8BCD`,
1044
- inDict: (id) => `\u6765\u6E90:${id}`,
1139
+ inDict: (name) => `\u6765\u6E90:${name}`,
1045
1140
  mistakes: (n, date) => `\u9519\u8FC7 ${n} \u6B21 (\u6700\u8FD1 ${date})`,
1046
1141
  footer: "\u8F93\u5165\u8FC7\u6EE4 \xB7 \u2191/\u2193 \u9009\u62E9 \xB7 Esc \u8FD4\u56DE"
1047
1142
  },
@@ -1073,10 +1168,20 @@ var zh = {
1073
1168
  accuracy: "\u51C6\u786E\u7387",
1074
1169
  elapsed: (t) => `\u8017\u65F6 ${t}`
1075
1170
  },
1171
+ pause: {
1172
+ title: "\u5DF2\u6682\u505C",
1173
+ chapter: (c, t) => `\u7B2C ${c}/${t} \u7AE0`,
1174
+ progress: (completed, total) => `${completed}/${total}`,
1175
+ hint: "Enter \u7EE7\u7EED \xB7 Esc \u8FD4\u56DE\u83DC\u5355"
1176
+ },
1177
+ summary: {
1178
+ loopAgain: "\u518D\u6765\u4E00\u904D",
1179
+ nextChapter: "\u4E0B\u4E00\u7AE0",
1180
+ reviewMistakes: "\u590D\u4E60\u9519\u8BCD",
1181
+ backMenu: "\u8FD4\u56DE\u83DC\u5355"
1182
+ },
1076
1183
  footers: {
1077
- typing: "Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD \xB7 Ctrl+C \u9000\u51FA",
1078
- paused: "[r] \u7EE7\u7EED \xB7 [q] \u8FD4\u56DE\u83DC\u5355",
1079
- summary: "[n] \u4E0B\u4E00\u7AE0 \xB7 [m] \u590D\u4E60\u9519\u8BCD \xB7 [q] \u8FD4\u56DE\u83DC\u5355"
1184
+ typing: "Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD"
1080
1185
  },
1081
1186
  errors: {
1082
1187
  noMistakes: "\u9519\u8BCD\u672C\u662F\u7A7A\u7684\u3002\u5148\u7EC3\u4E60\u51E0\u7AE0\u5427\u3002",
@@ -1096,7 +1201,49 @@ var zh = {
1096
1201
  accuracy: "\u51C6\u786E\u7387",
1097
1202
  wpm: "\u901F\u5EA6",
1098
1203
  newMistakes: "\u65B0\u9519\u8BCD",
1099
- farewell: "\u4E0B\u6B21\u89C1\u3002"
1204
+ farewell: "\u4E0B\u6B21\u89C1\u3002",
1205
+ notPracticed: "\u672C\u6B21\u672A\u7EC3\u4E60"
1206
+ },
1207
+ help: {
1208
+ title: "\u5E2E\u52A9",
1209
+ subtitle: "\u5168\u90E8\u5FEB\u6377\u952E",
1210
+ sections: {
1211
+ main: "\u4E3B\u83DC\u5355",
1212
+ practice: "\u7EC3\u4E60",
1213
+ dict: "\u8BCD\u5178",
1214
+ config: "\u8BBE\u7F6E",
1215
+ stats: "\u7EDF\u8BA1",
1216
+ word: "\u67E5\u8BCD",
1217
+ global: "\u5168\u5C40"
1218
+ },
1219
+ keys: {
1220
+ navigate: "\u2191/\u2193 \u79FB\u52A8\u9009\u9879",
1221
+ select: "Enter \u786E\u8BA4 / \u7EE7\u7EED",
1222
+ letterJump: "\u5B57\u6BCD\u952E \u76F4\u8FBE\u83DC\u5355\u9879",
1223
+ pause: "Esc \u6682\u505C\u7EC3\u4E60",
1224
+ skip: "Ctrl+N \u8DF3\u8FC7\u5F53\u524D\u8BCD(\u4E0D\u8BA1\u9519)",
1225
+ replay: "Tab \u91CD\u64AD\u53D1\u97F3",
1226
+ resume: "Enter \u7EE7\u7EED\u7EC3\u4E60",
1227
+ backMenu: "Esc \u8FD4\u56DE\u4E0A\u4E00\u5C4F",
1228
+ backScreen: "Esc \u5173\u95ED\u9762\u677F / \u8FD4\u56DE",
1229
+ nextChapter: "Enter \u4E0B\u4E00\u7AE0",
1230
+ reviewMistakes: "m \u590D\u4E60\u9519\u8BCD",
1231
+ filter: "\u8F93\u5165 \u8FC7\u6EE4\u5217\u8868",
1232
+ itemActions: "Enter \u5F39\u51FA\u52A8\u4F5C\u9762\u677F",
1233
+ moreActions: "Ctrl+K \u5F39\u51FA\u66F4\u591A\u529F\u80FD",
1234
+ cycleWindow: "\u2190/\u2192 \u5207\u6362\u65E5\u7A97\u53E3",
1235
+ stealthToggle: "Ctrl+I \u5207\u6362\u6478\u9C7C\u4FE1\u606F\u884C",
1236
+ helpScreen: "? \u6253\u5F00\u672C\u5E2E\u52A9\u9875",
1237
+ quit: "Ctrl+C \u7ACB\u5373\u9000\u51FA"
1238
+ },
1239
+ footer: "Esc \u8FD4\u56DE"
1240
+ },
1241
+ stealth: {
1242
+ paused: "paused",
1243
+ chapterDone: "chapter done",
1244
+ resumeHint: "Enter resume \xB7 Esc menu",
1245
+ nextHint: "Enter next \xB7 Esc menu",
1246
+ infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
1100
1247
  }
1101
1248
  };
1102
1249
 
@@ -1145,13 +1292,55 @@ function pickStrings(pref) {
1145
1292
  return { lang, t: lang === "zh" ? zh : en };
1146
1293
  }
1147
1294
 
1295
+ // src/ui/registry-context.tsx
1296
+ import { createContext as createContext5, useContext as useContext5, useEffect as useEffect3, useState as useState5 } from "react";
1297
+ import { jsx as jsx6 } from "react/jsx-runtime";
1298
+ var RegistryContext = createContext5(null);
1299
+ function RegistryProvider({ children }) {
1300
+ const [registry, setRegistry] = useState5(null);
1301
+ const [byId, setById] = useState5(/* @__PURE__ */ new Map());
1302
+ useEffect3(() => {
1303
+ let cancelled = false;
1304
+ (async () => {
1305
+ try {
1306
+ const reg = await loadRegistry();
1307
+ if (cancelled) return;
1308
+ const map = /* @__PURE__ */ new Map();
1309
+ for (const e of reg) map.set(e.id, e);
1310
+ setRegistry(reg);
1311
+ setById(map);
1312
+ } catch {
1313
+ if (!cancelled) {
1314
+ setRegistry([]);
1315
+ setById(/* @__PURE__ */ new Map());
1316
+ }
1317
+ }
1318
+ })();
1319
+ return () => {
1320
+ cancelled = true;
1321
+ };
1322
+ }, []);
1323
+ return /* @__PURE__ */ jsx6(RegistryContext.Provider, { value: { registry, byId }, children });
1324
+ }
1325
+ function useRegistry() {
1326
+ const ctx = useContext5(RegistryContext);
1327
+ if (!ctx) throw new Error("useRegistry must be used inside RegistryProvider");
1328
+ return ctx;
1329
+ }
1330
+ function useDictName(id) {
1331
+ const { byId } = useRegistry();
1332
+ if (!id) return "";
1333
+ const entry = byId.get(id);
1334
+ return entry?.name ?? id;
1335
+ }
1336
+
1148
1337
  // src/ui/screens/MainMenu.tsx
1149
- import { useState as useState5 } from "react";
1338
+ import { useState as useState6 } from "react";
1150
1339
  import { Box as Box3, Text as Text2, useApp, useInput } from "ink";
1151
1340
 
1152
1341
  // src/ui/components/BigWord.tsx
1153
1342
  import { Box as Box2, Text, useStdout as useStdout2 } from "ink";
1154
- import { jsx as jsx6, jsxs } from "react/jsx-runtime";
1343
+ import { jsx as jsx7, jsxs } from "react/jsx-runtime";
1155
1344
  var PALETTE = {
1156
1345
  accent: "#5eead4",
1157
1346
  muted: "#6b7280",
@@ -1166,23 +1355,27 @@ function BigWord({ target, typed, error = false, hideTarget = false }) {
1166
1355
  const cols = stdout?.columns ?? 80;
1167
1356
  const chars = [...target];
1168
1357
  const typedChars = [...typed];
1169
- const sep = cols < 60 ? " " : " ";
1170
- return /* @__PURE__ */ jsx6(Box2, { justifyContent: "center", children: chars.map((ch, i) => {
1171
- const isTyped = i < typedChars.length;
1172
- const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
1173
- const color = isTyped ? PALETTE.accent : error ? PALETTE.error : PALETTE.muted;
1174
- return /* @__PURE__ */ jsxs(Text, { bold: true, color, underline: !isTyped && error, children: [
1175
- display,
1176
- i < chars.length - 1 ? sep : ""
1177
- ] }, i);
1178
- }) });
1179
- }
1180
- function BoldSpaced({ text, color = PALETTE.text }) {
1181
- const chars = [...text];
1182
- return /* @__PURE__ */ jsx6(Box2, { children: chars.map((ch, i) => /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
1183
- ch,
1184
- i < chars.length - 1 ? " " : ""
1185
- ] }, i)) });
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
+ ] });
1186
1379
  }
1187
1380
 
1188
1381
  // src/util/text.ts
@@ -1200,45 +1393,85 @@ function visibleWidth2(s) {
1200
1393
  return w;
1201
1394
  }
1202
1395
 
1396
+ // src/util/dict-name.ts
1397
+ function truncateName(name, max) {
1398
+ if (visibleWidth2(name) <= max) return name;
1399
+ let out = "";
1400
+ let w = 0;
1401
+ for (const ch of name) {
1402
+ const code = ch.codePointAt(0);
1403
+ const cw = code > 11904 && code < 64256 ? 2 : 1;
1404
+ if (w + cw > max - 1) break;
1405
+ out += ch;
1406
+ w += cw;
1407
+ }
1408
+ return out + "\u2026";
1409
+ }
1410
+
1203
1411
  // src/ui/screens/MainMenu.tsx
1204
- import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
1412
+ import { jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
1205
1413
  function MainMenu({ cfg }) {
1206
- const [selected, setSelected] = useState5(0);
1414
+ const [selected, setSelected] = useState6(0);
1207
1415
  const { exit } = useApp();
1208
1416
  const nav = useNav();
1209
1417
  const audio = useAudioStatus();
1210
1418
  const t = useStrings();
1419
+ const defaultDictName = useDictName(cfg.defaultDict);
1211
1420
  const m = t.mainMenu.items;
1421
+ const startPractice = (stealth) => {
1422
+ if (cfg.defaultDict) {
1423
+ nav.navigate({
1424
+ name: "practice",
1425
+ params: {
1426
+ dictId: cfg.defaultDict,
1427
+ chapterIndex: 0,
1428
+ mode: cfg.defaultMode,
1429
+ stealth
1430
+ }
1431
+ });
1432
+ } else {
1433
+ nav.navigate({ name: "dict", params: { pickerMode: "choose-then-practice" } });
1434
+ }
1435
+ };
1212
1436
  const items = [
1213
1437
  {
1214
1438
  key: "p",
1215
1439
  label: m.practiceLabel,
1216
- hint: cfg.defaultDict ? m.practiceHintWith(cfg.defaultDict) : m.practiceHintNone,
1217
- run: () => {
1218
- if (cfg.defaultDict) {
1219
- nav.navigate({
1220
- name: "practice",
1221
- params: { dictId: cfg.defaultDict, chapterIndex: 0, mode: cfg.defaultMode }
1222
- });
1223
- } else {
1224
- nav.navigate({ name: "dict", params: { pickerMode: "choose-then-practice" } });
1225
- }
1226
- }
1227
- },
1440
+ hint: cfg.defaultDict ? m.practiceHintWith(truncateName(defaultDictName, 24)) : m.practiceHintNone,
1441
+ run: () => startPractice(cfg.stealth === "default")
1442
+ }
1443
+ ];
1444
+ if (cfg.stealth === "menu" || cfg.stealth === "default") {
1445
+ items.push({
1446
+ key: "b",
1447
+ label: m.stealthLabel,
1448
+ hint: m.stealthHint,
1449
+ run: () => startPractice(true)
1450
+ });
1451
+ }
1452
+ items.push(
1228
1453
  { key: "d", label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: "dict" }) },
1229
1454
  { key: "w", label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: "word" }) },
1230
1455
  { key: "s", label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: "stats" }) },
1231
1456
  { key: "c", label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: "config" }) },
1232
1457
  { key: "q", label: m.quitLabel, hint: m.quitHint, run: () => exit() }
1233
- ];
1458
+ );
1234
1459
  const labelW = Math.max(...items.map((it) => visibleWidth2(it.label))) + 4;
1235
1460
  useInput((input, key) => {
1461
+ if (key.escape) {
1462
+ exit();
1463
+ return;
1464
+ }
1236
1465
  if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);
1237
1466
  if (key.downArrow) setSelected((i) => (i + 1) % items.length);
1238
1467
  if (key.return) {
1239
1468
  items[selected].run();
1240
1469
  return;
1241
1470
  }
1471
+ if (input === "?") {
1472
+ nav.navigate({ name: "help" });
1473
+ return;
1474
+ }
1242
1475
  for (const it of items) {
1243
1476
  if (input === it.key) {
1244
1477
  it.run();
@@ -1248,39 +1481,42 @@ function MainMenu({ cfg }) {
1248
1481
  });
1249
1482
  return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", children: [
1250
1483
  /* @__PURE__ */ jsxs2(Box3, { children: [
1251
- /* @__PURE__ */ jsx7(Text2, { bold: true, color: PALETTE.accent, children: t.app.title }),
1484
+ /* @__PURE__ */ jsx8(Text2, { bold: true, color: PALETTE.accent, children: t.app.title }),
1252
1485
  /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
1253
1486
  " \xB7 ",
1254
1487
  t.app.subtitle
1255
1488
  ] })
1256
1489
  ] }),
1257
- /* @__PURE__ */ jsx7(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
1490
+ /* @__PURE__ */ jsx8(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
1258
1491
  const active = i === selected;
1259
1492
  const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(it.label)));
1260
1493
  return /* @__PURE__ */ jsxs2(Box3, { children: [
1261
- /* @__PURE__ */ jsx7(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
1494
+ /* @__PURE__ */ jsx8(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
1262
1495
  /* @__PURE__ */ jsxs2(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
1263
1496
  "[",
1264
1497
  it.key,
1265
1498
  "]"
1266
1499
  ] }),
1267
- /* @__PURE__ */ jsx7(Text2, { children: " " }),
1500
+ /* @__PURE__ */ jsx8(Text2, { children: " " }),
1268
1501
  /* @__PURE__ */ jsxs2(Text2, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
1269
1502
  it.label,
1270
1503
  pad
1271
1504
  ] }),
1272
- /* @__PURE__ */ jsx7(Text2, { color: PALETTE.muted, children: it.hint })
1505
+ /* @__PURE__ */ jsx8(Text2, { color: PALETTE.muted, children: it.hint })
1273
1506
  ] }, it.key);
1274
1507
  }) }),
1275
- /* @__PURE__ */ jsx7(Box3, { marginTop: 2, children: /* @__PURE__ */ jsx7(Text2, { color: PALETTE.muted, children: cfg.defaultDict ? t.mainMenu.defaultDict(cfg.defaultDict) : t.mainMenu.noDefault }) }),
1276
- /* @__PURE__ */ jsx7(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text2, { color: PALETTE.muted, children: t.mainMenu.hint }) }),
1277
- audio.warning && /* @__PURE__ */ jsx7(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text2, { color: PALETTE.warning, children: t.audio.noPlayer }) })
1508
+ /* @__PURE__ */ jsx8(Box3, { marginTop: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
1509
+ t.mainMenu.hint,
1510
+ " \xB7 ",
1511
+ t.mainMenu.helpHint
1512
+ ] }) }),
1513
+ audio.warning && /* @__PURE__ */ jsx8(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text2, { color: PALETTE.warning, children: t.audio.noPlayer }) })
1278
1514
  ] });
1279
1515
  }
1280
1516
 
1281
1517
  // src/ui/screens/PracticeScreen.tsx
1282
- import { useState as useState7, useEffect as useEffect5, useRef as useRef3 } from "react";
1283
- import { Box as Box4, Text as Text3, useApp as useApp3, useInput as useInput3 } from "ink";
1518
+ import { useState as useState8, useEffect as useEffect6, useRef as useRef3 } from "react";
1519
+ import { Box as Box5, Text as Text4, useApp as useApp3, useInput as useInput3 } from "ink";
1284
1520
 
1285
1521
  // src/util/shuffle.ts
1286
1522
  function shuffle(arr, rng = Math.random) {
@@ -1479,7 +1715,7 @@ function topN(book, n) {
1479
1715
  }
1480
1716
 
1481
1717
  // src/ui/hooks/useWordLoop.ts
1482
- import { useEffect as useEffect3, useReducer, useRef, useState as useState6 } from "react";
1718
+ import { useEffect as useEffect4, useReducer, useRef, useState as useState7 } from "react";
1483
1719
  import { useInput as useInput2, useApp as useApp2 } from "ink";
1484
1720
  function reducer(state2, action) {
1485
1721
  if (action.type === "start") {
@@ -1513,7 +1749,7 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
1513
1749
  lastEffect: null
1514
1750
  }));
1515
1751
  const completedRef = useRef(false);
1516
- const [tick, setTick] = useState6(0);
1752
+ const [tick, setTick] = useState7(0);
1517
1753
  const { exit } = useApp2();
1518
1754
  useInput2(
1519
1755
  (input, key) => {
@@ -1545,13 +1781,13 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
1545
1781
  },
1546
1782
  { isActive: enabled }
1547
1783
  );
1548
- useEffect3(() => {
1784
+ useEffect4(() => {
1549
1785
  if (state2.session.finishedAt !== null && !completedRef.current) {
1550
1786
  completedRef.current = true;
1551
1787
  onComplete(state2.session);
1552
1788
  }
1553
1789
  }, [state2.session, onComplete]);
1554
- useEffect3(() => {
1790
+ useEffect4(() => {
1555
1791
  if (state2.session.finishedAt !== null) return;
1556
1792
  const id = setInterval(() => setTick((t) => t + 1), 1e3);
1557
1793
  return () => clearInterval(id);
@@ -1560,10 +1796,10 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
1560
1796
  }
1561
1797
 
1562
1798
  // src/ui/hooks/useAudio.ts
1563
- import { useEffect as useEffect4, useRef as useRef2 } from "react";
1799
+ import { useEffect as useEffect5, useRef as useRef2 } from "react";
1564
1800
  function useAudio(opts) {
1565
1801
  const initedRef = useRef2(false);
1566
- useEffect4(() => {
1802
+ useEffect5(() => {
1567
1803
  if (initedRef.current) return;
1568
1804
  initedRef.current = true;
1569
1805
  initAudio(!opts.enabled).catch(() => void 0);
@@ -1737,16 +1973,80 @@ function useSessionPersistence(meta) {
1737
1973
  );
1738
1974
  }
1739
1975
 
1976
+ // 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";
1979
+ var TYPED = "#d4d4d4";
1980
+ var UNTYPED = "#808080";
1981
+ var DIM = "#6b6b6b";
1982
+ function fmtTime(ms) {
1983
+ const total = Math.floor(ms / 1e3);
1984
+ const m = Math.floor(total / 60);
1985
+ const s = total % 60;
1986
+ return `${m}:${String(s).padStart(2, "0")}`;
1987
+ }
1988
+ function StealthTyping(props) {
1989
+ const t = useStrings();
1990
+ const target = [...props.target];
1991
+ 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 })
2018
+ ] });
2019
+ }
2020
+ function StealthPaused() {
2021
+ 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 })
2027
+ ] });
2028
+ }
2029
+ function StealthSummary(props) {
2030
+ 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 })
2037
+ ] });
2038
+ }
2039
+
1740
2040
  // src/ui/screens/PracticeScreen.tsx
1741
- import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
2041
+ import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
1742
2042
  function PracticeScreen({ params }) {
1743
2043
  const { dictId, chapterIndex, mode } = params;
1744
2044
  const { cfg } = useAppState();
1745
2045
  const t = useStrings();
1746
- const [phase, setPhase] = useState7("loading");
1747
- const [loaded, setLoaded] = useState7(null);
1748
- const [errorMsg, setErrorMsg] = useState7(null);
1749
- useEffect5(() => {
2046
+ const [phase, setPhase] = useState8("loading");
2047
+ const [loaded, setLoaded] = useState8(null);
2048
+ const [errorMsg, setErrorMsg] = useState8(null);
2049
+ useEffect6(() => {
1750
2050
  let cancelled = false;
1751
2051
  setPhase("loading");
1752
2052
  setLoaded(null);
@@ -1789,13 +2089,13 @@ function PracticeScreen({ params }) {
1789
2089
  };
1790
2090
  }, [dictId, chapterIndex, mode, cfg.chapterSize, t]);
1791
2091
  if (phase === "loading") {
1792
- return /* @__PURE__ */ jsx8(Centered, { text: t.practice.loading, color: PALETTE.muted });
2092
+ return /* @__PURE__ */ jsx10(Centered, { text: t.practice.loading, color: PALETTE.muted });
1793
2093
  }
1794
2094
  if (phase === "error") {
1795
- return /* @__PURE__ */ jsx8(ErrorView, { msg: errorMsg ?? t.practice.errors.unknown });
2095
+ return /* @__PURE__ */ jsx10(ErrorView, { msg: errorMsg ?? t.practice.errors.unknown });
1796
2096
  }
1797
2097
  if (!loaded) return null;
1798
- return /* @__PURE__ */ jsx8(
2098
+ return /* @__PURE__ */ jsx10(
1799
2099
  PracticeRunner,
1800
2100
  {
1801
2101
  params,
@@ -1803,7 +2103,7 @@ function PracticeScreen({ params }) {
1803
2103
  phase,
1804
2104
  setPhase
1805
2105
  },
1806
- `${dictId}-${chapterIndex}-${mode}`
2106
+ `${dictId}-${chapterIndex}-${mode}-${params.stealth ? "s" : "n"}`
1807
2107
  );
1808
2108
  }
1809
2109
  function PracticeRunner({
@@ -1813,19 +2113,22 @@ function PracticeRunner({
1813
2113
  setPhase
1814
2114
  }) {
1815
2115
  const { dictId, chapterIndex, mode } = params;
2116
+ const stealth = params.stealth === true;
1816
2117
  const { cfg } = useAppState();
1817
2118
  const nav = useNav();
1818
2119
  const { exit } = useApp3();
1819
2120
  const goBack = () => nav.stack.length > 1 ? nav.back() : exit();
1820
2121
  const persist = useSessionPersistence({ dictId, chapterIndex, mode });
2122
+ const dictName = useDictName(dictId);
1821
2123
  const audio = useAudio({
1822
- enabled: cfg.sounds.master,
2124
+ enabled: !stealth && cfg.sounds.master,
1823
2125
  accent: cfg.accent,
1824
- autoplayPronunciation: cfg.autoplayPronunciation
2126
+ autoplayPronunciation: !stealth && cfg.autoplayPronunciation
1825
2127
  });
1826
2128
  const finishedRef = useRef3(false);
1827
2129
  const lastEffectRef = useRef3(null);
1828
2130
  const lastIndexRef = useRef3(-1);
2131
+ const [infoVisible, setInfoVisible] = useState8(false);
1829
2132
  const { session, lastEffect, tick } = useWordLoop({
1830
2133
  playlist: loaded.playlist,
1831
2134
  enabled: phase === "typing",
@@ -1838,12 +2141,13 @@ function PracticeRunner({
1838
2141
  });
1839
2142
  },
1840
2143
  onEscape: () => setPhase(phase === "paused" ? "typing" : "paused"),
1841
- onTab: () => {
2144
+ onTab: stealth ? void 0 : () => {
1842
2145
  const cur = session.current ? loaded.playlist[session.current.wordIndex] : void 0;
1843
2146
  if (cur) void audio.pronounce(cur.name);
1844
2147
  }
1845
2148
  });
1846
- useEffect5(() => {
2149
+ useEffect6(() => {
2150
+ if (stealth) return;
1847
2151
  if (lastEffect === null) return;
1848
2152
  if (lastEffect === lastEffectRef.current) return;
1849
2153
  lastEffectRef.current = lastEffect;
@@ -1853,8 +2157,9 @@ function PracticeRunner({
1853
2157
  if (cfg.sounds.feedback) audio.correct();
1854
2158
  if (cfg.sounds.keystroke) audio.keystroke();
1855
2159
  }
1856
- }, [lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
1857
- useEffect5(() => {
2160
+ }, [stealth, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
2161
+ useEffect6(() => {
2162
+ if (stealth) return;
1858
2163
  const idx = session.current?.wordIndex ?? -1;
1859
2164
  if (idx === -1) return;
1860
2165
  if (idx === lastIndexRef.current) return;
@@ -1863,63 +2168,148 @@ function PracticeRunner({
1863
2168
  const next = loaded.playlist[idx + 1];
1864
2169
  if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);
1865
2170
  if (next) audio.prefetch(next.name);
1866
- }, [session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
2171
+ }, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
1867
2172
  void tick;
1868
2173
  useInput3(
1869
- (input) => {
1870
- if (input === "r") setPhase("typing");
1871
- if (input === "q") goBack();
2174
+ (input, key) => {
2175
+ if (key.ctrl && input === "i") {
2176
+ setInfoVisible((v) => !v);
2177
+ return;
2178
+ }
2179
+ },
2180
+ { isActive: stealth && phase === "typing" }
2181
+ );
2182
+ useInput3(
2183
+ (_input, key) => {
2184
+ if (key.return) {
2185
+ setPhase("typing");
2186
+ return;
2187
+ }
2188
+ if (key.escape) {
2189
+ goBack();
2190
+ return;
2191
+ }
1872
2192
  },
1873
2193
  { isActive: phase === "paused" }
1874
2194
  );
1875
2195
  useInput3(
1876
- (input) => {
1877
- if (input === "q") {
2196
+ (input, key) => {
2197
+ if (key.escape) {
1878
2198
  goBack();
1879
2199
  return;
1880
2200
  }
1881
- if (input === "n") {
2201
+ if (key.return) {
1882
2202
  const nextIdx = chapterIndex + 1;
1883
2203
  if (mode === "loop") {
1884
- nav.replace({ name: "practice", params: { dictId, chapterIndex, mode } });
2204
+ nav.replace({
2205
+ name: "practice",
2206
+ params: { dictId, chapterIndex, mode, stealth: params.stealth }
2207
+ });
1885
2208
  } else if (mode === "review" || nextIdx >= loaded.totalChapters) {
1886
2209
  goBack();
1887
2210
  } else {
1888
- nav.replace({ name: "practice", params: { dictId, chapterIndex: nextIdx, mode } });
2211
+ nav.replace({
2212
+ name: "practice",
2213
+ params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth }
2214
+ });
1889
2215
  }
1890
2216
  return;
1891
2217
  }
1892
2218
  if (input === "m") {
1893
- nav.replace({ name: "practice", params: { dictId, chapterIndex: 0, mode: "review" } });
2219
+ nav.replace({
2220
+ name: "practice",
2221
+ params: { dictId, chapterIndex: 0, mode: "review", stealth: params.stealth }
2222
+ });
1894
2223
  return;
1895
2224
  }
1896
2225
  },
1897
2226
  { isActive: phase === "summary" }
1898
2227
  );
1899
- if (phase === "paused") return /* @__PURE__ */ jsx8(PausedView, {});
1900
- if (phase === "summary") {
1901
- return /* @__PURE__ */ jsx8(
2228
+ const completed = session.results.length;
2229
+ const errors = session.results.reduce((a, r) => a + r.errors, 0);
2230
+ const elapsedMs = Date.now() - session.startedAt;
2231
+ const minutes = elapsedMs / 6e4;
2232
+ const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
2233
+ const summary = phase === "summary" ? sessionSummary(session) : null;
2234
+ if (stealth) {
2235
+ if (phase === "paused") return /* @__PURE__ */ jsx10(StealthPaused, {});
2236
+ if (phase === "summary" && summary) {
2237
+ const sMinutes = summary.durationMs / 6e4;
2238
+ const sWpm = sMinutes > 0 ? Math.round(summary.wordCount / sMinutes * 10) / 10 : 0;
2239
+ const sErrWords = Object.keys(summary.perWordErrors).length;
2240
+ const sAcc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - sErrWords) / summary.wordCount);
2241
+ const sAccPct = Math.round(sAcc * 1e3) / 10;
2242
+ return /* @__PURE__ */ jsx10(
2243
+ StealthSummary,
2244
+ {
2245
+ wordCount: summary.wordCount,
2246
+ errors: summary.errors,
2247
+ durationMs: summary.durationMs,
2248
+ wpm: sWpm,
2249
+ accPct: sAccPct
2250
+ }
2251
+ );
2252
+ }
2253
+ const currentWord2 = session.current ? loaded.playlist[session.current.wordIndex] : loaded.playlist[loaded.playlist.length - 1];
2254
+ const inputState2 = session.current?.input ?? { target: "", typed: "", errorsThisWord: 0 };
2255
+ const errWords = new Set(
2256
+ session.results.filter((r) => r.errors > 0).map((r) => r.word)
2257
+ ).size;
2258
+ const accFrac = completed === 0 ? 1 : Math.max(0, (completed - errWords) / completed);
2259
+ const accPct = Math.round(accFrac * 1e3) / 10;
2260
+ const chapterLabel = mode === "review" ? "review" : `ch ${chapterIndex + 1}/${loaded.totalChapters}`;
2261
+ return /* @__PURE__ */ jsx10(
2262
+ StealthTyping,
2263
+ {
2264
+ target: currentWord2?.name ?? "",
2265
+ typed: inputState2.typed,
2266
+ hideTarget: mode === "dictation",
2267
+ phonetic: pickPhonetic(currentWord2, cfg.accent),
2268
+ translation: currentWord2?.trans ?? [],
2269
+ error: lastEffect === "wrong",
2270
+ info: {
2271
+ visible: infoVisible,
2272
+ dictName: truncateName(dictName, 24),
2273
+ chapterLabel,
2274
+ completed,
2275
+ total: loaded.playlist.length,
2276
+ wpm,
2277
+ accPct
2278
+ }
2279
+ }
2280
+ );
2281
+ }
2282
+ if (phase === "paused") {
2283
+ return /* @__PURE__ */ jsx10(
2284
+ PausedView,
2285
+ {
2286
+ dictName,
2287
+ chapterIndex,
2288
+ totalChapters: loaded.totalChapters,
2289
+ mode,
2290
+ completed,
2291
+ total: loaded.playlist.length
2292
+ }
2293
+ );
2294
+ }
2295
+ if (phase === "summary" && summary) {
2296
+ return /* @__PURE__ */ jsx10(
1902
2297
  SummaryView,
1903
2298
  {
1904
- dictId,
2299
+ dictName,
1905
2300
  chapterIndex,
1906
2301
  totalChapters: loaded.totalChapters,
1907
2302
  mode,
1908
- summary: sessionSummary(session)
2303
+ summary
1909
2304
  }
1910
2305
  );
1911
2306
  }
1912
2307
  const currentWord = session.current ? loaded.playlist[session.current.wordIndex] : loaded.playlist[loaded.playlist.length - 1];
1913
2308
  const inputState = session.current?.input ?? { target: "", typed: "", errorsThisWord: 0 };
1914
- const elapsedMs = Date.now() - session.startedAt;
1915
- const completed = session.results.length;
1916
- const errors = session.results.reduce((a, r) => a + r.errors, 0);
1917
- const minutes = elapsedMs / 6e4;
1918
- const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
1919
- return /* @__PURE__ */ jsx8(
2309
+ return /* @__PURE__ */ jsx10(
1920
2310
  TypingLayout,
1921
2311
  {
1922
- dictId,
2312
+ dictName,
1923
2313
  chapterIndex,
1924
2314
  totalChapters: loaded.totalChapters,
1925
2315
  mode,
@@ -1943,7 +2333,7 @@ function pickPhonetic(word, accent) {
1943
2333
  const p = accent === "us" ? word.usphone : word.ukphone;
1944
2334
  return p ? `/${p}/` : null;
1945
2335
  }
1946
- function fmtTime(ms) {
2336
+ function fmtTime2(ms) {
1947
2337
  const total = Math.floor(ms / 1e3);
1948
2338
  const m = Math.floor(total / 60);
1949
2339
  const s = total % 60;
@@ -1952,11 +2342,11 @@ function fmtTime(ms) {
1952
2342
  function TypingLayout(props) {
1953
2343
  const t = useStrings();
1954
2344
  const progressFrac = props.total === 0 ? 0 : props.completed / props.total;
1955
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
1956
- /* @__PURE__ */ jsx8(
2345
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2346
+ /* @__PURE__ */ jsx10(
1957
2347
  StatusBar,
1958
2348
  {
1959
- dictId: props.dictId,
2349
+ dictName: props.dictName,
1960
2350
  chapterIndex: props.chapterIndex,
1961
2351
  totalChapters: props.totalChapters,
1962
2352
  mode: props.mode,
@@ -1966,8 +2356,8 @@ function TypingLayout(props) {
1966
2356
  elapsedMs: props.elapsedMs
1967
2357
  }
1968
2358
  ),
1969
- /* @__PURE__ */ jsxs3(Box4, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
1970
- /* @__PURE__ */ jsx8(
2359
+ /* @__PURE__ */ jsxs4(Box5, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
2360
+ /* @__PURE__ */ jsx10(
1971
2361
  BigWord,
1972
2362
  {
1973
2363
  target: props.target,
@@ -1976,17 +2366,17 @@ function TypingLayout(props) {
1976
2366
  hideTarget: props.hideTarget
1977
2367
  }
1978
2368
  ),
1979
- props.phonetic && /* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text3, { italic: true, color: PALETTE.muted, children: props.phonetic }) }),
1980
- props.translation.length > 0 && /* @__PURE__ */ jsx8(Box4, { marginTop: 1, flexDirection: "column", alignItems: "center", children: props.translation.slice(0, 2).map((tr, i) => /* @__PURE__ */ jsx8(Text3, { color: PALETTE.primary, children: tr }, i)) })
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)) })
1981
2371
  ] }),
1982
- /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
1983
- /* @__PURE__ */ jsx8(ProgressBar, { frac: progressFrac }),
1984
- /* @__PURE__ */ jsx8(Box4, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: PALETTE.muted, children: [
2372
+ /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
2373
+ /* @__PURE__ */ jsx10(ProgressBar, { frac: progressFrac }),
2374
+ /* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
1985
2375
  props.completed,
1986
2376
  "/",
1987
2377
  props.total,
1988
2378
  " \xB7 ",
1989
- fmtTime(props.elapsedMs),
2379
+ fmtTime2(props.elapsedMs),
1990
2380
  " \xB7 ",
1991
2381
  props.wpm,
1992
2382
  " ",
@@ -1996,7 +2386,7 @@ function TypingLayout(props) {
1996
2386
  " ",
1997
2387
  t.practice.statCards.errors
1998
2388
  ] }) }),
1999
- /* @__PURE__ */ jsx8(Box4, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.footers.typing }) })
2389
+ /* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.footers.typing }) })
2000
2390
  ] })
2001
2391
  ] });
2002
2392
  }
@@ -2004,12 +2394,13 @@ function StatusBar(props) {
2004
2394
  const t = useStrings();
2005
2395
  const modeName = t.practice.modes[props.mode];
2006
2396
  const accentName = t.practice.accents[props.accent];
2007
- const left = props.mode === "review" ? `${props.dictId} \xB7 ${t.practice.reviewLabel} \xB7 ${accentName}` : `${props.dictId} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName} \xB7 ${accentName}`;
2008
- const right = `${props.completed}/${props.total} \xB7 ${fmtTime(props.elapsedMs)}`;
2009
- return /* @__PURE__ */ jsxs3(Box4, { children: [
2010
- /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: left }),
2011
- /* @__PURE__ */ jsx8(Box4, { flexGrow: 1 }),
2012
- /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: right })
2397
+ const name = truncateName(props.dictName, 20);
2398
+ 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
+ const right = `${props.completed}/${props.total} \xB7 ${fmtTime2(props.elapsedMs)}`;
2400
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
2401
+ /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: left }),
2402
+ /* @__PURE__ */ jsx10(Box5, { flexGrow: 1 }),
2403
+ /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: right })
2013
2404
  ] });
2014
2405
  }
2015
2406
  function ProgressBar({ frac }) {
@@ -2017,38 +2408,43 @@ function ProgressBar({ frac }) {
2017
2408
  const width = Math.max(20, Math.min(72, cols - 16));
2018
2409
  const filled = Math.round(width * Math.max(0, Math.min(1, frac)));
2019
2410
  const empty = width - filled;
2020
- return /* @__PURE__ */ jsxs3(Box4, { justifyContent: "center", children: [
2021
- /* @__PURE__ */ jsx8(Text3, { color: PALETTE.accent, children: "\u2501".repeat(filled) }),
2022
- /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: "\u2500".repeat(empty) })
2411
+ return /* @__PURE__ */ jsxs4(Box5, { justifyContent: "center", children: [
2412
+ /* @__PURE__ */ jsx10(Text4, { color: PALETTE.accent, children: "\u2501".repeat(filled) }),
2413
+ /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: "\u2500".repeat(empty) })
2023
2414
  ] });
2024
2415
  }
2025
- function PausedView() {
2416
+ function PausedView(props) {
2026
2417
  const t = useStrings();
2027
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2028
- /* @__PURE__ */ jsx8(BoldSpaced, { text: t.practice.paused, color: PALETTE.warning }),
2029
- /* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.footers.paused }) })
2418
+ const frac = props.total === 0 ? 0 : props.completed / props.total;
2419
+ 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: [
2421
+ /* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.warning, children: t.practice.pause.title }),
2422
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: subtitle }) }),
2423
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(ProgressBar, { frac }) }),
2424
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.pause.progress(props.completed, props.total) }) }),
2425
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.pause.hint }) })
2030
2426
  ] });
2031
2427
  }
2032
2428
  function ErrorView({ msg }) {
2033
2429
  const t = useStrings();
2034
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2035
- /* @__PURE__ */ jsx8(Text3, { color: PALETTE.error, children: msg }),
2036
- /* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: PALETTE.muted, children: [
2037
- "[q] ",
2430
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2431
+ /* @__PURE__ */ jsx10(Text4, { color: PALETTE.error, children: msg }),
2432
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
2433
+ "Esc ",
2038
2434
  t.common.back
2039
2435
  ] }) }),
2040
- /* @__PURE__ */ jsx8(BackKey, {})
2436
+ /* @__PURE__ */ jsx10(BackKey, {})
2041
2437
  ] });
2042
2438
  }
2043
2439
  function BackKey() {
2044
2440
  const nav = useNav();
2045
- useInput3((input, key) => {
2046
- if (input === "q" || key.escape) nav.back();
2441
+ useInput3((_input, key) => {
2442
+ if (key.escape) nav.back();
2047
2443
  });
2048
2444
  return null;
2049
2445
  }
2050
2446
  function Centered({ text, color }) {
2051
- return /* @__PURE__ */ jsx8(Box4, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx8(Text3, { color, children: text }) });
2447
+ return /* @__PURE__ */ jsx10(Box5, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx10(Text4, { color, children: text }) });
2052
2448
  }
2053
2449
  function SummaryView(props) {
2054
2450
  const { summary } = props;
@@ -2059,13 +2455,16 @@ function SummaryView(props) {
2059
2455
  const accPct = Math.round(acc * 1e3) / 10;
2060
2456
  const t = useStrings();
2061
2457
  const modeName = t.practice.modes[props.mode];
2062
- const subtitle = props.mode === "review" ? `${props.dictId} \xB7 ${t.practice.reviewLabel}` : `${props.dictId} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName}`;
2063
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 1, width: "100%", height: "100%", children: [
2064
- /* @__PURE__ */ jsx8(BoldSpaced, { text: t.practice.chapterComplete, color: PALETTE.success }),
2065
- /* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: subtitle }) }),
2066
- /* @__PURE__ */ jsxs3(Box4, { marginTop: 3, flexDirection: "row", justifyContent: "center", children: [
2067
- /* @__PURE__ */ jsx8(StatCard, { label: t.practice.statCards.words, value: String(summary.wordCount), color: PALETTE.text }),
2068
- /* @__PURE__ */ jsx8(
2458
+ const name = truncateName(props.dictName, 20);
2459
+ const subtitle = props.mode === "review" ? `${name} \xB7 ${t.practice.reviewLabel}` : `${name} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName}`;
2460
+ 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
+ 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: [
2463
+ /* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.success, children: t.practice.chapterComplete }),
2464
+ /* @__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: [
2466
+ /* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.words, value: String(summary.wordCount), color: PALETTE.text }),
2467
+ /* @__PURE__ */ jsx10(
2069
2468
  StatCard,
2070
2469
  {
2071
2470
  label: t.practice.statCards.errors,
@@ -2073,36 +2472,107 @@ function SummaryView(props) {
2073
2472
  color: summary.errors > 0 ? PALETTE.error : PALETTE.muted
2074
2473
  }
2075
2474
  ),
2076
- /* @__PURE__ */ jsx8(StatCard, { label: t.practice.statCards.wpm, value: String(wpm), color: PALETTE.accent }),
2077
- /* @__PURE__ */ jsx8(StatCard, { label: t.practice.statCards.accuracy, value: `${accPct}%`, color: PALETTE.accent })
2475
+ /* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.wpm, value: String(wpm), color: PALETTE.accent }),
2476
+ /* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.accuracy, value: `${accPct}%`, color: PALETTE.accent })
2078
2477
  ] }),
2079
- /* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.statCards.elapsed(fmtTime(summary.durationMs)) }) }),
2080
- /* @__PURE__ */ jsx8(Box4, { flexGrow: 1 }),
2081
- /* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.footers.summary }) })
2478
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.statCards.elapsed(fmtTime2(summary.durationMs)) }) }),
2479
+ /* @__PURE__ */ jsx10(Box5, { flexGrow: 1 }),
2480
+ /* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: footer }) })
2082
2481
  ] });
2083
2482
  }
2084
2483
  function StatCard({ label, value, color }) {
2085
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", marginX: 3, children: [
2086
- /* @__PURE__ */ jsx8(Text3, { bold: true, color, children: value }),
2087
- /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: label })
2484
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", marginX: 3, children: [
2485
+ /* @__PURE__ */ jsx10(Text4, { bold: true, color, children: value }),
2486
+ /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: label })
2088
2487
  ] });
2089
2488
  }
2090
2489
 
2091
2490
  // src/ui/screens/DictBrowser.tsx
2092
- import { useEffect as useEffect6, useState as useState8 } from "react";
2093
- import { Box as Box5, Text as Text4, useInput as useInput4 } from "ink";
2094
- import { Fragment, jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
2491
+ import { useEffect as useEffect7, useMemo as useMemo2, useState as useState10 } from "react";
2492
+ import { Box as Box7, Text as Text6, useInput as useInput5, useStdout as useStdout3 } from "ink";
2493
+
2494
+ // src/ui/components/ActionPanel.tsx
2495
+ import { useState as useState9 } from "react";
2496
+ import { Box as Box6, Text as Text5, useInput as useInput4 } from "ink";
2497
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
2498
+ function ActionPanel({ title, items, onClose }) {
2499
+ const enabledIndices = items.map((it, i) => it.disabled ? -1 : i).filter((i) => i >= 0);
2500
+ const initial = enabledIndices[0] ?? 0;
2501
+ const [selected, setSelected] = useState9(initial);
2502
+ useInput4((input, key) => {
2503
+ if (key.escape) {
2504
+ onClose();
2505
+ return;
2506
+ }
2507
+ if (key.upArrow) {
2508
+ const cur = enabledIndices.indexOf(selected);
2509
+ const next = enabledIndices[(cur - 1 + enabledIndices.length) % enabledIndices.length];
2510
+ if (next !== void 0) setSelected(next);
2511
+ return;
2512
+ }
2513
+ if (key.downArrow) {
2514
+ const cur = enabledIndices.indexOf(selected);
2515
+ const next = enabledIndices[(cur + 1) % enabledIndices.length];
2516
+ if (next !== void 0) setSelected(next);
2517
+ return;
2518
+ }
2519
+ if (key.return) {
2520
+ const item = items[selected];
2521
+ if (item && !item.disabled) {
2522
+ void item.run();
2523
+ }
2524
+ return;
2525
+ }
2526
+ for (let i = 0; i < items.length; i++) {
2527
+ const it = items[i];
2528
+ if (it.disabled) continue;
2529
+ if (it.key && input === it.key) {
2530
+ void it.run();
2531
+ return;
2532
+ }
2533
+ }
2534
+ });
2535
+ const maxLabel = Math.max(...items.map((it) => it.label.length));
2536
+ const width = Math.max(maxLabel + 8, title.length + 4, 24);
2537
+ return /* @__PURE__ */ jsxs5(
2538
+ Box6,
2539
+ {
2540
+ flexDirection: "column",
2541
+ borderStyle: "round",
2542
+ borderColor: PALETTE.accent,
2543
+ paddingX: 2,
2544
+ paddingY: 1,
2545
+ width,
2546
+ children: [
2547
+ /* @__PURE__ */ jsx11(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx11(Text5, { bold: true, color: PALETTE.accent, children: title }) }),
2548
+ items.map((it, i) => {
2549
+ const active = i === selected;
2550
+ const color = it.disabled ? PALETTE.muted : active ? PALETTE.text : PALETTE.muted;
2551
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
2552
+ /* @__PURE__ */ jsx11(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
2553
+ /* @__PURE__ */ jsx11(Text5, { bold: active, color, children: it.label })
2554
+ ] }, i);
2555
+ }),
2556
+ /* @__PURE__ */ jsx11(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text5, { color: PALETTE.muted, children: "\u2191/\u2193 \xB7 Enter \xB7 Esc" }) })
2557
+ ]
2558
+ }
2559
+ );
2560
+ }
2561
+
2562
+ // src/ui/screens/DictBrowser.tsx
2563
+ import { Fragment, jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
2095
2564
  function DictBrowser({ params }) {
2096
2565
  const nav = useNav();
2097
2566
  const { cfg, setCfg } = useAppState();
2098
2567
  const t = useStrings();
2099
- const [rows, setRows] = useState8([]);
2100
- const [loading, setLoading] = useState8(true);
2101
- const [selected, setSelected] = useState8(0);
2102
- const [filter, setFilter] = useState8("");
2103
- const [filterFocus, setFilterFocus] = useState8(false);
2104
- const [pending, setPending] = useState8(null);
2105
- const [tick, setTick] = useState8(0);
2568
+ const { stdout } = useStdout3();
2569
+ const [rows, setRows] = useState10([]);
2570
+ const [loading, setLoading] = useState10(true);
2571
+ const [selected, setSelected] = useState10(0);
2572
+ const [filter, setFilter] = useState10("");
2573
+ const [pending, setPending] = useState10(null);
2574
+ const [tick, setTick] = useState10(0);
2575
+ const [panel, setPanel] = useState10(null);
2106
2576
  const refresh = async () => {
2107
2577
  const reg = await loadRegistry();
2108
2578
  const flagged = await Promise.all(
@@ -2111,141 +2581,221 @@ function DictBrowser({ params }) {
2111
2581
  setRows(flagged);
2112
2582
  setLoading(false);
2113
2583
  };
2114
- useEffect6(() => {
2584
+ useEffect7(() => {
2115
2585
  void refresh();
2116
2586
  }, [tick]);
2117
- const filtered = filter ? rows.filter((r) => filterRegistry([r.entry], filter).length > 0) : rows;
2587
+ const filtered = useMemo2(
2588
+ () => filter ? rows.filter((r) => filterRegistry([r.entry], filter).length > 0) : rows,
2589
+ [filter, rows]
2590
+ );
2118
2591
  const safeSelected = Math.max(0, Math.min(filtered.length - 1, selected));
2119
2592
  const current = filtered[safeSelected];
2120
- useInput4((input, key) => {
2121
- if (filterFocus) {
2122
- if (key.escape || key.return) {
2123
- setFilterFocus(false);
2124
- return;
2593
+ const rowsTotal = stdout?.rows ?? 24;
2594
+ const visibleH = Math.max(6, rowsTotal - 8);
2595
+ const half = Math.floor(visibleH / 2);
2596
+ const start2 = Math.max(0, Math.min(filtered.length - visibleH, safeSelected - half));
2597
+ const end = Math.min(filtered.length, start2 + visibleH);
2598
+ const goPractice = (id) => {
2599
+ nav.replace({
2600
+ name: "practice",
2601
+ params: {
2602
+ dictId: id,
2603
+ chapterIndex: 0,
2604
+ mode: cfg.defaultMode,
2605
+ stealth: cfg.stealth === "default"
2125
2606
  }
2126
- if (key.backspace || key.delete) {
2127
- setFilter((f) => f.slice(0, -1));
2128
- return;
2607
+ });
2608
+ };
2609
+ const doSetDefault = async (id, navigate = true) => {
2610
+ await setCfg({ ...cfg, defaultDict: id });
2611
+ setPanel(null);
2612
+ if (navigate) {
2613
+ if (params?.pickerMode === "choose-then-practice") {
2614
+ goPractice(id);
2615
+ } else {
2616
+ nav.back();
2617
+ }
2618
+ }
2619
+ };
2620
+ const doDelete = (id) => {
2621
+ setPanel(null);
2622
+ setPending({ kind: "removing", id });
2623
+ void (async () => {
2624
+ try {
2625
+ await removeDictionary(id);
2626
+ setPending(null);
2627
+ setTick((n) => n + 1);
2628
+ } catch (err) {
2629
+ setPending({ kind: "error", id, msg: err.message });
2129
2630
  }
2130
- if (input && !key.ctrl && !key.meta) {
2131
- setFilter((f) => f + input);
2631
+ })();
2632
+ };
2633
+ const doPull = (id) => {
2634
+ setPanel(null);
2635
+ setPending({ kind: "pulling", id });
2636
+ void (async () => {
2637
+ try {
2638
+ await pullDictionary(id);
2639
+ setPending(null);
2640
+ setTick((n) => n + 1);
2641
+ } catch (err) {
2642
+ setPending({ kind: "error", id, msg: err.message });
2132
2643
  }
2644
+ })();
2645
+ };
2646
+ const doRefreshList = () => {
2647
+ setPanel(null);
2648
+ setPending({ kind: "refreshing" });
2649
+ setTick((n) => n + 1);
2650
+ setPending(null);
2651
+ };
2652
+ useInput5((input, key) => {
2653
+ if (panel !== null) return;
2654
+ if (key.escape) {
2655
+ nav.back();
2133
2656
  return;
2134
2657
  }
2135
- if (key.upArrow) setSelected((i) => Math.max(0, i - 1));
2136
- if (key.downArrow) setSelected((i) => Math.min(filtered.length - 1, i + 1));
2137
- if (input === "/") {
2138
- setFilterFocus(true);
2658
+ if (key.upArrow) {
2659
+ setSelected((i) => Math.max(0, i - 1));
2139
2660
  return;
2140
2661
  }
2141
- if (key.escape || input === "b") {
2142
- nav.back();
2662
+ if (key.downArrow) {
2663
+ setSelected((i) => Math.min(filtered.length - 1, i + 1));
2143
2664
  return;
2144
2665
  }
2145
- if (!current) return;
2146
- if (key.return) {
2147
- void (async () => {
2148
- await setCfg({ ...cfg, defaultDict: current.entry.id });
2149
- if (params?.pickerMode === "choose-then-practice") {
2150
- nav.replace({
2151
- name: "practice",
2152
- params: { dictId: current.entry.id, chapterIndex: 0, mode: cfg.defaultMode }
2153
- });
2154
- } else {
2155
- nav.back();
2156
- }
2157
- })();
2666
+ if (key.ctrl && input === "k") {
2667
+ setPanel("more");
2158
2668
  return;
2159
2669
  }
2160
- if (input === "p") {
2161
- nav.replace({
2162
- name: "practice",
2163
- params: { dictId: current.entry.id, chapterIndex: 0, mode: cfg.defaultMode }
2164
- });
2670
+ if (key.return) {
2671
+ if (current) setPanel("item");
2165
2672
  return;
2166
2673
  }
2167
- if (input === "r" && current.local) {
2168
- setPending({ kind: "removing", id: current.entry.id });
2169
- void (async () => {
2170
- try {
2171
- await removeDictionary(current.entry.id);
2172
- setPending(null);
2173
- setTick((n) => n + 1);
2174
- } catch (err) {
2175
- setPending({ kind: "error", id: current.entry.id, msg: err.message });
2176
- }
2177
- })();
2674
+ if (key.backspace || key.delete) {
2675
+ setFilter((f) => f.slice(0, -1));
2676
+ setSelected(0);
2178
2677
  return;
2179
2678
  }
2180
- if (input === "u") {
2181
- setPending({ kind: "pulling", id: current.entry.id });
2182
- void (async () => {
2183
- try {
2184
- await pullDictionary(current.entry.id);
2185
- setPending(null);
2186
- setTick((n) => n + 1);
2187
- } catch (err) {
2188
- setPending({ kind: "error", id: current.entry.id, msg: err.message });
2189
- }
2190
- })();
2679
+ if (input && !key.ctrl && !key.meta && input.length === 1) {
2680
+ setFilter((f) => f + input);
2681
+ setSelected(0);
2191
2682
  }
2192
2683
  });
2193
2684
  if (loading) {
2194
- return /* @__PURE__ */ jsx9(Box5, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: t.dict.loading }) });
2685
+ return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.loading }) });
2195
2686
  }
2196
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2197
- /* @__PURE__ */ jsxs4(Box5, { children: [
2198
- /* @__PURE__ */ jsx9(Text4, { bold: true, color: PALETTE.accent, children: t.dict.title }),
2199
- /* @__PURE__ */ jsx9(Box5, { flexGrow: 1 }),
2200
- /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: filterFocus ? `/ ${filter}_` : filter ? t.dict.filterPrompt(filter) : t.dict.entries(filtered.length) })
2687
+ const itemPanelItems = current ? [
2688
+ {
2689
+ label: t.dict.action.setDefault,
2690
+ run: () => void doSetDefault(current.entry.id, params?.pickerMode !== void 0)
2691
+ },
2692
+ {
2693
+ label: t.dict.action.practice,
2694
+ run: () => goPractice(current.entry.id)
2695
+ },
2696
+ {
2697
+ label: t.dict.action.delete,
2698
+ disabled: !current.local,
2699
+ run: () => doDelete(current.entry.id)
2700
+ },
2701
+ { label: t.common.cancel, run: () => setPanel(null) }
2702
+ ] : [];
2703
+ const morePanelItems = [
2704
+ {
2705
+ label: t.dict.command.pull,
2706
+ disabled: !current,
2707
+ run: () => current && doPull(current.entry.id)
2708
+ },
2709
+ {
2710
+ label: t.dict.command.import,
2711
+ disabled: true,
2712
+ run: () => void 0
2713
+ },
2714
+ { label: t.dict.command.refreshList, run: () => doRefreshList() },
2715
+ { label: t.common.cancel, run: () => setPanel(null) }
2716
+ ];
2717
+ if (panel === "item" && current) {
2718
+ return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(
2719
+ ActionPanel,
2720
+ {
2721
+ title: `${t.dict.action.title} \xB7 ${current.entry.name}`,
2722
+ items: itemPanelItems,
2723
+ onClose: () => setPanel(null)
2724
+ }
2725
+ ) });
2726
+ }
2727
+ if (panel === "more") {
2728
+ return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(
2729
+ ActionPanel,
2730
+ {
2731
+ title: t.dict.command.title,
2732
+ items: morePanelItems,
2733
+ onClose: () => setPanel(null)
2734
+ }
2735
+ ) });
2736
+ }
2737
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2738
+ /* @__PURE__ */ jsxs6(Box7, { children: [
2739
+ /* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.accent, children: t.dict.title }),
2740
+ /* @__PURE__ */ jsx12(Box7, { flexGrow: 1 }),
2741
+ /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: filter ? `${t.dict.filterPlaceholder}: ${filter}_` : `${t.dict.filterPlaceholder}_` }),
2742
+ /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
2743
+ " ",
2744
+ t.dict.entries(filtered.length)
2745
+ ] })
2201
2746
  ] }),
2202
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexGrow: 1, children: [
2203
- /* @__PURE__ */ jsx9(Box5, { flexDirection: "column", width: "75%", paddingRight: 1, children: filtered.slice(Math.max(0, safeSelected - 8), safeSelected + 16).map((row, vi) => {
2204
- const i = Math.max(0, safeSelected - 8) + vi;
2747
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexGrow: 1, children: [
2748
+ /* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "75%", paddingRight: 1, children: filtered.slice(start2, end).map((row, vi) => {
2749
+ const i = start2 + vi;
2205
2750
  const active = i === safeSelected;
2206
2751
  const isDefault = cfg.defaultDict === row.entry.id;
2207
- return /* @__PURE__ */ jsxs4(Box5, { children: [
2208
- /* @__PURE__ */ jsx9(Box5, { width: 2, children: /* @__PURE__ */ jsx9(Text4, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }) }),
2209
- /* @__PURE__ */ jsx9(Box5, { width: 2, children: /* @__PURE__ */ jsx9(Text4, { color: row.local ? PALETTE.accent : PALETTE.muted, children: row.local ? "\u25CF" : "\u25CB" }) }),
2210
- /* @__PURE__ */ jsx9(Box5, { width: 2, children: /* @__PURE__ */ jsx9(Text4, { color: isDefault ? PALETTE.success : PALETTE.muted, children: isDefault ? "\u2605" : " " }) }),
2211
- /* @__PURE__ */ jsx9(Box5, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text4, { bold: active, color: active ? PALETTE.text : PALETTE.muted, wrap: "wrap", children: row.entry.name }) }),
2212
- /* @__PURE__ */ jsx9(Box5, { width: 6, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: String(row.entry.length).padStart(5) }) })
2752
+ return /* @__PURE__ */ jsxs6(Box7, { children: [
2753
+ /* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }) }),
2754
+ /* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: row.local ? PALETTE.accent : PALETTE.muted, children: row.local ? "\u25CF" : "\u25CB" }) }),
2755
+ /* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: isDefault ? PALETTE.success : PALETTE.muted, children: isDefault ? "\u2605" : " " }) }),
2756
+ /* @__PURE__ */ jsx12(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx12(Text6, { bold: active, color: active ? PALETTE.text : PALETTE.muted, wrap: "truncate", children: row.entry.name }) }),
2757
+ /* @__PURE__ */ jsx12(Box7, { width: 6, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: String(row.entry.length).padStart(5) }) })
2213
2758
  ] }, row.entry.id);
2214
2759
  }) }),
2215
- /* @__PURE__ */ jsx9(Box5, { flexDirection: "column", width: "25%", paddingLeft: 1, children: current && /* @__PURE__ */ jsxs4(Fragment, { children: [
2216
- /* @__PURE__ */ jsx9(Text4, { bold: true, color: PALETTE.text, wrap: "wrap", children: current.entry.name }),
2217
- /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: current.entry.id }),
2218
- /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, wrap: "wrap", children: [
2760
+ /* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "25%", paddingLeft: 1, children: current && /* @__PURE__ */ jsxs6(Fragment, { children: [
2761
+ /* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.text, wrap: "wrap", children: current.entry.name }),
2762
+ /* @__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: [
2219
2764
  current.entry.language,
2220
2765
  " \xB7 ",
2221
2766
  current.entry.category
2222
2767
  ] }) }),
2223
- /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: t.dict.wordsLabel(current.entry.length) }) }),
2224
- current.entry.description && /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.primary, wrap: "wrap", children: current.entry.description }) }),
2225
- current.entry.tags.length > 0 && /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, wrap: "wrap", children: t.dict.tagsLabel(current.entry.tags.join(", ")) }) }),
2226
- /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: current.local ? PALETTE.accent : PALETTE.muted, children: current.local ? t.dict.local : t.dict.notLocal }) }),
2227
- cfg.defaultDict === current.entry.id && /* @__PURE__ */ jsx9(Box5, { children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.success, children: t.dict.defaultMark }) })
2768
+ /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.wordsLabel(current.entry.length) }) }),
2769
+ current.entry.description && /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.primary, wrap: "wrap", children: current.entry.description }) }),
2770
+ current.entry.tags.length > 0 && /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, wrap: "wrap", children: t.dict.tagsLabel(current.entry.tags.join(", ")) }) }),
2771
+ /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: current.local ? PALETTE.accent : PALETTE.muted, children: current.local ? t.dict.local : t.dict.notLocal }) }),
2772
+ cfg.defaultDict === current.entry.id && /* @__PURE__ */ jsx12(Box7, { children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.success, children: t.dict.defaultMark }) })
2228
2773
  ] }) })
2229
2774
  ] }),
2230
- pending && /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, children: [
2231
- pending.kind === "pulling" && /* @__PURE__ */ jsx9(Text4, { color: PALETTE.warning, children: t.dict.pulling(pending.id) }),
2232
- pending.kind === "removing" && /* @__PURE__ */ jsx9(Text4, { color: PALETTE.warning, children: t.dict.removing(pending.id) }),
2233
- pending.kind === "error" && /* @__PURE__ */ jsx9(Text4, { color: PALETTE.error, children: t.dict.errorOn(pending.id, pending.msg) })
2775
+ pending && /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, children: [
2776
+ pending.kind === "pulling" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.warning, children: t.dict.pulling(pending.id) }),
2777
+ 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: [
2779
+ t.dict.command.refreshList,
2780
+ "\u2026"
2781
+ ] }),
2782
+ pending.kind === "error" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.error, children: t.dict.errorOn(pending.id, pending.msg) })
2234
2783
  ] }),
2235
- /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: t.dict.footer }) })
2784
+ /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.footer }) })
2236
2785
  ] });
2237
2786
  }
2238
2787
 
2239
2788
  // src/ui/screens/ConfigEditor.tsx
2240
- import { useState as useState9 } from "react";
2241
- import { Box as Box6, Text as Text5, useInput as useInput5 } from "ink";
2242
- import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
2789
+ import { useState as useState11 } from "react";
2790
+ import { Box as Box8, Text as Text7, useInput as useInput6 } from "ink";
2791
+ import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
2243
2792
  var FIELDS = [
2244
2793
  { kind: "dictRef", path: "defaultDict", labelKey: "defaultDict" },
2245
2794
  { kind: "enum", path: "defaultMode", labelKey: "defaultMode", options: ["order", "dictation", "review", "random", "loop"] },
2246
2795
  { kind: "enum", path: "accent", labelKey: "accent", options: ["us", "uk"] },
2247
2796
  { kind: "enum", path: "language", labelKey: "language", options: ["auto", "zh", "en"] },
2248
2797
  { kind: "enum", path: "mirror", labelKey: "mirror", options: ["jsdelivr", "github"] },
2798
+ { kind: "enum", path: "stealth", labelKey: "stealth", options: ["off", "menu", "default"] },
2249
2799
  { kind: "int", path: "chapterSize", labelKey: "chapterSize", min: 1, max: 200 },
2250
2800
  { kind: "bool", path: "autoplayPronunciation", labelKey: "autoplayPronunciation" },
2251
2801
  { kind: "bool", path: "sounds.master", labelKey: "soundsMaster" },
@@ -2263,10 +2813,11 @@ function ConfigEditor() {
2263
2813
  const nav = useNav();
2264
2814
  const { cfg, setCfg } = useAppState();
2265
2815
  const t = useStrings();
2266
- const [selected, setSelected] = useState9(0);
2267
- const [editing, setEditing] = useState9(false);
2268
- const [draft, setDraft] = useState9("");
2269
- const [error, setError] = useState9(null);
2816
+ const defaultDictName = useDictName(cfg.defaultDict);
2817
+ const [selected, setSelected] = useState11(0);
2818
+ const [editing, setEditing] = useState11(false);
2819
+ const [draft, setDraft] = useState11("");
2820
+ const [error, setError] = useState11(null);
2270
2821
  const field = FIELDS[selected];
2271
2822
  const currentValue = getByPath2(cfg, field.path);
2272
2823
  const commit = async (raw) => {
@@ -2279,7 +2830,7 @@ function ConfigEditor() {
2279
2830
  setError(err.message);
2280
2831
  }
2281
2832
  };
2282
- useInput5((input, key) => {
2833
+ useInput6((input, key) => {
2283
2834
  if (editing && field.kind === "string") {
2284
2835
  if (key.escape) {
2285
2836
  setEditing(false);
@@ -2314,7 +2865,7 @@ function ConfigEditor() {
2314
2865
  if (/^[0-9]$/.test(input)) setDraft((d) => d + input);
2315
2866
  return;
2316
2867
  }
2317
- if (key.escape || input === "b") {
2868
+ if (key.escape) {
2318
2869
  nav.back();
2319
2870
  return;
2320
2871
  }
@@ -2350,35 +2901,51 @@ function ConfigEditor() {
2350
2901
  }
2351
2902
  });
2352
2903
  const labelW = Math.max(...FIELDS.map((f) => visibleWidth2(t.config.fields[f.labelKey]))) + 4;
2353
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2354
- /* @__PURE__ */ jsx10(Text5, { bold: true, color: PALETTE.accent, children: t.config.title }),
2355
- /* @__PURE__ */ jsx10(Box6, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: FIELDS.map((f, i) => {
2904
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2905
+ /* @__PURE__ */ jsx13(Text7, { bold: true, color: PALETTE.accent, children: t.config.title }),
2906
+ /* @__PURE__ */ jsx13(Box8, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: FIELDS.map((f, i) => {
2356
2907
  const active = i === selected;
2357
2908
  const value = getByPath2(cfg, f.path);
2358
- const display = renderValue(f, value, active && editing ? draft : null, t);
2909
+ const display = renderValue(
2910
+ f,
2911
+ value,
2912
+ active && editing ? draft : null,
2913
+ t,
2914
+ f.path === "defaultDict" ? defaultDictName : ""
2915
+ );
2359
2916
  const label = t.config.fields[f.labelKey];
2360
2917
  const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
2361
- return /* @__PURE__ */ jsxs5(Box6, { children: [
2362
- /* @__PURE__ */ jsx10(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
2363
- /* @__PURE__ */ jsxs5(Text5, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
2918
+ return /* @__PURE__ */ jsxs7(Box8, { children: [
2919
+ /* @__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: [
2364
2921
  label,
2365
2922
  pad
2366
2923
  ] }),
2367
- /* @__PURE__ */ jsx10(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: display })
2924
+ /* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: display })
2368
2925
  ] }, f.path);
2369
2926
  }) }),
2370
- error && /* @__PURE__ */ jsx10(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: PALETTE.error, children: [
2927
+ error && /* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.error, children: [
2371
2928
  "! ",
2372
2929
  error
2373
2930
  ] }) }),
2374
- /* @__PURE__ */ jsx10(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text5, { color: PALETTE.muted, children: hintFor(field, editing, t) }) })
2931
+ /* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text7, { color: PALETTE.muted, children: hintFor(field, editing, t) }) })
2375
2932
  ] });
2376
2933
  }
2377
- function renderValue(field, value, draft, t) {
2934
+ function renderValue(field, value, draft, t, dictDisplayName) {
2378
2935
  if (draft !== null) return `${draft}_`;
2379
2936
  if (field.kind === "bool") return value ? `\u2713 ${t.common.on}` : `\u2717 ${t.common.off}`;
2380
- if (field.kind === "dictRef") return String(value ?? "\u2014");
2381
- if (field.kind === "enum") return `< ${value} >`;
2937
+ if (field.kind === "dictRef") {
2938
+ if (!value) return "\u2014";
2939
+ return truncateName(dictDisplayName || String(value), 24);
2940
+ }
2941
+ if (field.kind === "enum") {
2942
+ if (field.path === "stealth") {
2943
+ const v = String(value);
2944
+ const label = t.config.enumValues.stealth[v] ?? String(value);
2945
+ return `< ${label} >`;
2946
+ }
2947
+ return `< ${value} >`;
2948
+ }
2382
2949
  return String(value ?? "");
2383
2950
  }
2384
2951
  function hintFor(field, editing, t) {
@@ -2390,40 +2957,40 @@ function hintFor(field, editing, t) {
2390
2957
  }
2391
2958
 
2392
2959
  // src/ui/screens/StatsViewer.tsx
2393
- import { useEffect as useEffect7, useState as useState10 } from "react";
2394
- import { Box as Box7, Text as Text6, useInput as useInput6 } from "ink";
2395
- import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
2960
+ 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";
2396
2963
  var DAY_WINDOWS = [7, 14, 30, 90];
2397
2964
  function StatsViewer() {
2398
2965
  const nav = useNav();
2399
2966
  const t = useStrings();
2400
- const [sessions, setSessions] = useState10(null);
2401
- const [book, setBook] = useState10(null);
2402
- const [windowIdx, setWindowIdx] = useState10(1);
2403
- useEffect7(() => {
2967
+ const [sessions, setSessions] = useState12(null);
2968
+ const [book, setBook] = useState12(null);
2969
+ const [windowIdx, setWindowIdx] = useState12(1);
2970
+ useEffect8(() => {
2404
2971
  void (async () => {
2405
2972
  const [s, b] = await Promise.all([loadSessions(), loadMistakes()]);
2406
2973
  setSessions(s);
2407
2974
  setBook(b);
2408
2975
  })();
2409
2976
  }, []);
2410
- useInput6((input, key) => {
2411
- if (key.escape || input === "b" || input === "q") {
2977
+ useInput7((_input, key) => {
2978
+ if (key.escape) {
2412
2979
  nav.back();
2413
2980
  return;
2414
2981
  }
2415
- if (input === "n") setWindowIdx((i) => (i + 1) % DAY_WINDOWS.length);
2416
- if (input === "N") setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);
2982
+ if (key.rightArrow) setWindowIdx((i) => (i + 1) % DAY_WINDOWS.length);
2983
+ if (key.leftArrow) setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);
2417
2984
  });
2418
2985
  if (!sessions || !book) {
2419
- return /* @__PURE__ */ jsx11(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.loading }) });
2986
+ return /* @__PURE__ */ jsx14(Box9, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.loading }) });
2420
2987
  }
2421
2988
  if (sessions.length === 0) {
2422
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2423
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.none }),
2424
- /* @__PURE__ */ jsx11(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.nonePractice }) }),
2425
- /* @__PURE__ */ jsx11(Box7, { marginTop: 2, children: /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
2426
- "[q] ",
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: [
2993
+ "Esc ",
2427
2994
  t.common.back
2428
2995
  ] }) })
2429
2996
  ] });
@@ -2442,98 +3009,162 @@ function StatsViewer() {
2442
3009
  const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
2443
3010
  const recent = sessions.slice(-5).reverse();
2444
3011
  const top = topN(book, 8);
2445
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2446
- /* @__PURE__ */ jsx11(Text6, { bold: true, color: PALETTE.accent, children: t.stats.title }),
2447
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
2448
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.lifetime }),
2449
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, children: [
2450
- /* @__PURE__ */ jsx11(Stat, { label: t.stats.sessions, value: String(sessions.length) }),
2451
- /* @__PURE__ */ jsx11(Stat, { label: t.stats.words, value: String(totalWords) }),
2452
- /* @__PURE__ */ jsx11(Stat, { label: t.stats.errors, value: String(totalErrors) }),
2453
- /* @__PURE__ */ jsx11(Stat, { label: t.stats.wpm, value: String(overallWpm), accent: true }),
2454
- /* @__PURE__ */ jsx11(Stat, { label: t.stats.accuracy, value: `${Math.round(overallAcc * 1e3) / 10}%`, accent: true }),
2455
- /* @__PURE__ */ jsx11(Stat, { label: t.stats.streak, value: `${streak}d`, accent: true })
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 })
2456
3026
  ] })
2457
3027
  ] }),
2458
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 2, flexDirection: "column", children: [
2459
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.last(days) }),
2460
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
2461
- /* @__PURE__ */ jsxs6(Box7, { children: [
2462
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.wpm.padEnd(10) }),
2463
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.wpm)) }),
2464
- /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
2465
- " max ",
2466
- Math.round(Math.max(...buckets.map((b) => b.wpm)))
2467
- ] })
2468
- ] }),
2469
- /* @__PURE__ */ jsxs6(Box7, { children: [
2470
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.accuracy.padEnd(10) }),
2471
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.accuracy * 100)) })
2472
- ] }),
2473
- /* @__PURE__ */ jsxs6(Box7, { children: [
2474
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.sessions.padEnd(10) }),
2475
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.sessions)) })
2476
- ] })
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
+ )
2477
3056
  ] })
2478
3057
  ] }),
2479
- /* @__PURE__ */ jsxs6(Box7, { marginTop: 2, flexDirection: "column", children: [
2480
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.recent }),
2481
- recent.map((s, i) => /* @__PURE__ */ jsxs6(Box7, { children: [
2482
- /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
2483
- " ",
2484
- s.ts.replace("T", " ").slice(0, 16),
2485
- " "
2486
- ] }),
2487
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.text, children: s.dictId.padEnd(14) }),
2488
- /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
2489
- " ",
2490
- "ch",
2491
- String(s.chapter + 1).padStart(3),
2492
- " ",
2493
- s.mode.padEnd(9),
2494
- " ",
2495
- String(s.wordCount).padStart(3),
2496
- "w ",
2497
- s.errors,
2498
- "err ",
2499
- computeWPM(s),
2500
- "wpm ",
2501
- Math.round(accuracy(s) * 1e3) / 10,
2502
- "%"
2503
- ] })
2504
- ] }, i))
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))
2505
3061
  ] }),
2506
- top.length > 0 && /* @__PURE__ */ jsxs6(Box7, { marginTop: 2, flexDirection: "column", children: [
2507
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.topMistakes }),
2508
- top.map(([word, entry]) => /* @__PURE__ */ jsxs6(Box7, { children: [
2509
- /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.error, children: [
2510
- " ",
2511
- String(entry.count).padStart(3),
2512
- " "
2513
- ] }),
2514
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.text, children: word.padEnd(20) }),
2515
- /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: entry.dictIds.join(", ") })
2516
- ] }, word))
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(
3065
+ MistakeRow,
3066
+ {
3067
+ word,
3068
+ count: entry.count,
3069
+ dictIds: entry.dictIds,
3070
+ multiSuffix: t.stats.multiDictSuffix
3071
+ },
3072
+ word
3073
+ ))
3074
+ ] }),
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
+ ] }) })
3095
+ ] });
3096
+ }
3097
+ function RecentRow({
3098
+ session,
3099
+ units
3100
+ }) {
3101
+ const name = useDictName(session.dictId);
3102
+ const display = truncateName(name, 14);
3103
+ return /* @__PURE__ */ jsxs8(Box9, { children: [
3104
+ /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3105
+ " ",
3106
+ session.ts.replace("T", " ").slice(0, 16),
3107
+ " "
3108
+ ] }),
3109
+ /* @__PURE__ */ jsx14(Text8, { color: PALETTE.text, children: display.padEnd(14) }),
3110
+ /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3111
+ " ",
3112
+ "ch",
3113
+ String(session.chapter + 1).padStart(3),
3114
+ " ",
3115
+ session.mode.padEnd(9),
3116
+ " ",
3117
+ String(session.wordCount).padStart(3),
3118
+ units.words,
3119
+ " ",
3120
+ session.errors,
3121
+ units.errors,
3122
+ " ",
3123
+ computeWPM(session),
3124
+ units.wpm,
3125
+ " ",
3126
+ Math.round(accuracy(session) * 1e3) / 10,
3127
+ "%"
3128
+ ] })
3129
+ ] });
3130
+ }
3131
+ function MistakeRow({
3132
+ word,
3133
+ count,
3134
+ dictIds,
3135
+ multiSuffix
3136
+ }) {
3137
+ const firstId = dictIds[0] ?? "";
3138
+ const firstName = useDictName(firstId);
3139
+ const suffix = dictIds.length > 1 ? multiSuffix(dictIds.length - 1) : "";
3140
+ return /* @__PURE__ */ jsxs8(Box9, { children: [
3141
+ /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.error, children: [
3142
+ " ",
3143
+ String(count).padStart(3),
3144
+ " "
2517
3145
  ] }),
2518
- /* @__PURE__ */ jsx11(Box7, { flexGrow: 1 }),
2519
- /* @__PURE__ */ jsx11(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.footer }) })
3146
+ /* @__PURE__ */ jsx14(Text8, { color: PALETTE.text, children: word.padEnd(20) }),
3147
+ /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
3148
+ truncateName(firstName, 20),
3149
+ suffix
3150
+ ] })
2520
3151
  ] });
2521
3152
  }
2522
3153
  function Stat({ label, value, accent = false }) {
2523
- return /* @__PURE__ */ jsxs6(Box7, { marginRight: 3, children: [
2524
- /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
3154
+ return /* @__PURE__ */ jsxs8(Box9, { marginRight: 3, children: [
3155
+ /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
2525
3156
  label,
2526
3157
  " "
2527
3158
  ] }),
2528
- /* @__PURE__ */ jsx11(Text6, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
3159
+ /* @__PURE__ */ jsx14(Text8, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
2529
3160
  ] });
2530
3161
  }
2531
3162
 
2532
3163
  // src/ui/screens/WordLookup.tsx
2533
- import { useEffect as useEffect8, useState as useState11 } from "react";
2534
- import { Box as Box8, Text as Text7, useInput as useInput7 } from "ink";
3164
+ import { useEffect as useEffect9, useState as useState13 } from "react";
3165
+ import { Box as Box10, Text as Text9, useInput as useInput8 } from "ink";
2535
3166
  import { readdir } from "fs/promises";
2536
- import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
3167
+ import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2537
3168
  async function listLocalDictIds() {
2538
3169
  try {
2539
3170
  const files = await readdir(paths.dictsDir);
@@ -2545,12 +3176,12 @@ async function listLocalDictIds() {
2545
3176
  function WordLookup() {
2546
3177
  const nav = useNav();
2547
3178
  const t = useStrings();
2548
- const [query, setQuery] = useState11("");
2549
- const [allWords, setAllWords] = useState11([]);
2550
- const [book, setBook] = useState11({});
2551
- const [loading, setLoading] = useState11(true);
2552
- const [selected, setSelected] = useState11(0);
2553
- useEffect8(() => {
3179
+ const [query, setQuery] = useState13("");
3180
+ const [allWords, setAllWords] = useState13([]);
3181
+ const [book, setBook] = useState13({});
3182
+ const [loading, setLoading] = useState13(true);
3183
+ const [selected, setSelected] = useState13(0);
3184
+ useEffect9(() => {
2554
3185
  void (async () => {
2555
3186
  const ids = await listLocalDictIds();
2556
3187
  const collected = [];
@@ -2566,7 +3197,7 @@ function WordLookup() {
2566
3197
  }, []);
2567
3198
  const q = query.toLowerCase().trim();
2568
3199
  const filtered = q ? allWords.filter((h) => h.word.name.toLowerCase().includes(q)).slice(0, 50) : [];
2569
- useInput7((input, key) => {
3200
+ useInput8((input, key) => {
2570
3201
  if (key.escape) {
2571
3202
  nav.back();
2572
3203
  return;
@@ -2590,81 +3221,129 @@ function WordLookup() {
2590
3221
  }
2591
3222
  });
2592
3223
  if (loading) {
2593
- return /* @__PURE__ */ jsx12(Box8, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.indexing }) });
3224
+ return /* @__PURE__ */ jsx15(Box10, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.indexing }) });
2594
3225
  }
2595
3226
  if (allWords.length === 0) {
2596
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
2597
- /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.none }),
2598
- /* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.pullFirst }) }),
2599
- /* @__PURE__ */ jsx12(Box8, { marginTop: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
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: [
2600
3231
  "[Esc] ",
2601
3232
  t.common.back
2602
3233
  ] }) })
2603
3234
  ] });
2604
3235
  }
2605
3236
  const current = filtered[selected];
2606
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
2607
- /* @__PURE__ */ jsxs7(Box8, { children: [
2608
- /* @__PURE__ */ jsx12(Text7, { bold: true, color: PALETTE.accent, children: t.word.title }),
2609
- /* @__PURE__ */ jsx12(Box8, { flexGrow: 1 }),
2610
- /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.countAcross(allWords.length) })
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) })
2611
3242
  ] }),
2612
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, children: [
2613
- /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: "> " }),
2614
- /* @__PURE__ */ jsx12(Text7, { color: PALETTE.text, children: query }),
2615
- /* @__PURE__ */ jsx12(Text7, { color: PALETTE.accent, children: "_" })
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: "_" })
2616
3247
  ] }),
2617
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexGrow: 1, children: [
2618
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", width: "40%", children: [
2619
- filtered.map((h, i) => {
2620
- const active = i === selected;
2621
- return /* @__PURE__ */ jsxs7(Box8, { children: [
2622
- /* @__PURE__ */ jsx12(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
2623
- /* @__PURE__ */ jsx12(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: h.word.name.padEnd(20) }),
2624
- /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: h.dictId })
2625
- ] }, `${h.dictId}-${h.word.name}-${i}`);
2626
- }),
2627
- filtered.length === 0 && q && /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.noMatches(query) })
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) })
2628
3252
  ] }),
2629
- /* @__PURE__ */ jsx12(Box8, { flexDirection: "column", width: "60%", paddingLeft: 2, children: current && /* @__PURE__ */ jsxs7(Fragment2, { children: [
2630
- /* @__PURE__ */ jsx12(Text7, { bold: true, color: PALETTE.text, children: current.word.name }),
2631
- /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, children: [
2632
- current.word.usphone && /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
2633
- "US /",
2634
- current.word.usphone,
2635
- "/ "
2636
- ] }),
2637
- current.word.ukphone && /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
2638
- "UK /",
2639
- current.word.ukphone,
2640
- "/"
2641
- ] })
2642
- ] }),
2643
- /* @__PURE__ */ jsx12(Box8, { marginTop: 1, flexDirection: "column", children: (current.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.primary, children: [
2644
- "\xB7 ",
2645
- tr
2646
- ] }, i)) }),
2647
- /* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.inDict(current.dictId) }) }),
2648
- book[current.word.name] && /* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.error, children: t.word.mistakes(book[current.word.name].count, book[current.word.name].lastSeen.slice(0, 10)) }) })
2649
- ] }) })
3253
+ /* @__PURE__ */ jsx15(Box10, { flexDirection: "column", width: "60%", paddingLeft: 2, children: current && /* @__PURE__ */ jsx15(Detail, { hit: current, book }) })
3254
+ ] }),
3255
+ /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.footer }) })
3256
+ ] });
3257
+ }
3258
+ function HitRow({ hit, active }) {
3259
+ 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) })
3264
+ ] });
3265
+ }
3266
+ function Detail({ hit, book }) {
3267
+ const t = useStrings();
3268
+ const name = useDictName(hit.dictId);
3269
+ 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: [
3273
+ "US /",
3274
+ hit.word.usphone,
3275
+ "/ "
3276
+ ] }),
3277
+ hit.word.ukphone && /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.muted, children: [
3278
+ "UK /",
3279
+ hit.word.ukphone,
3280
+ "/"
3281
+ ] })
2650
3282
  ] }),
2651
- /* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.footer }) })
3283
+ /* @__PURE__ */ jsx15(Box10, { marginTop: 1, flexDirection: "column", children: (hit.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.primary, children: [
3284
+ "\xB7 ",
3285
+ tr
3286
+ ] }, 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)) }) })
3289
+ ] });
3290
+ }
3291
+
3292
+ // 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";
3295
+ function HelpScreen() {
3296
+ const nav = useNav();
3297
+ const t = useStrings();
3298
+ useInput9((_input, key) => {
3299
+ if (key.escape) nav.back();
3300
+ });
3301
+ const k = t.help.keys;
3302
+ const sections = [
3303
+ { title: t.help.sections.global, keys: [k.helpScreen, k.quit] },
3304
+ { title: t.help.sections.main, keys: [k.navigate, k.select, k.letterJump, k.helpScreen] },
3305
+ {
3306
+ title: t.help.sections.practice,
3307
+ keys: [k.pause, k.skip, k.replay, k.resume, k.nextChapter, k.reviewMistakes, k.stealthToggle, k.backMenu]
3308
+ },
3309
+ {
3310
+ title: t.help.sections.dict,
3311
+ keys: [k.navigate, k.filter, k.itemActions, k.moreActions, k.backScreen]
3312
+ },
3313
+ { title: t.help.sections.config, keys: [k.navigate, k.select, k.backMenu] },
3314
+ { title: t.help.sections.stats, keys: [k.cycleWindow, k.backMenu] },
3315
+ { title: t.help.sections.word, keys: [k.filter, k.navigate, k.backMenu] }
3316
+ ];
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 })
3322
+ ] }),
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: [
3326
+ " \xB7 ",
3327
+ line
3328
+ ] }) }, i))
3329
+ ] }, sec.title)) }),
3330
+ /* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.help.footer }) })
2652
3331
  ] });
2653
3332
  }
2654
3333
 
2655
3334
  // src/ui/App.tsx
2656
- import { jsx as jsx13 } from "react/jsx-runtime";
3335
+ import { jsx as jsx17 } from "react/jsx-runtime";
2657
3336
  function App({ initial, initialCfg }) {
2658
- return /* @__PURE__ */ jsx13(AppStateProvider, { initialCfg, children: /* @__PURE__ */ jsx13(LangBridge, { children: /* @__PURE__ */ jsx13(AudioStatusProvider, { disabled: !initialCfg.sounds.master, children: /* @__PURE__ */ jsx13(NavProvider, { initial, children: /* @__PURE__ */ jsx13(Fullscreen, { children: /* @__PURE__ */ jsx13(Router, {}) }) }) }) }) });
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, {}) }) }) }) }) }) });
2659
3338
  }
2660
3339
  function LangBridge({ children }) {
2661
3340
  const { cfg } = useAppState();
2662
- return /* @__PURE__ */ jsx13(StringsProvider, { pref: cfg.language, children });
3341
+ return /* @__PURE__ */ jsx17(StringsProvider, { pref: cfg.language, children });
2663
3342
  }
2664
3343
  function screenKey(frame) {
2665
3344
  if (frame.name === "practice") {
2666
3345
  const p = frame.params;
2667
- return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}`;
3346
+ return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? "s" : "n"}`;
2668
3347
  }
2669
3348
  return frame.name;
2670
3349
  }
@@ -2673,7 +3352,7 @@ function Router() {
2673
3352
  const { cfg } = useAppState();
2674
3353
  const { exit } = useApp4();
2675
3354
  const lastKeyRef = useRef4(null);
2676
- useInput8((input, key2) => {
3355
+ useInput10((input, key2) => {
2677
3356
  if (key2.ctrl && input === "c") exit();
2678
3357
  });
2679
3358
  const frame = nav.current;
@@ -2684,17 +3363,19 @@ function Router() {
2684
3363
  }
2685
3364
  switch (frame.name) {
2686
3365
  case "main":
2687
- return /* @__PURE__ */ jsx13(MainMenu, { cfg });
3366
+ return /* @__PURE__ */ jsx17(MainMenu, { cfg });
2688
3367
  case "practice":
2689
- return /* @__PURE__ */ jsx13(PracticeScreen, { params: frame.params });
3368
+ return /* @__PURE__ */ jsx17(PracticeScreen, { params: frame.params });
2690
3369
  case "dict":
2691
- return /* @__PURE__ */ jsx13(DictBrowser, { params: frame.params });
3370
+ return /* @__PURE__ */ jsx17(DictBrowser, { params: frame.params });
2692
3371
  case "config":
2693
- return /* @__PURE__ */ jsx13(ConfigEditor, {});
3372
+ return /* @__PURE__ */ jsx17(ConfigEditor, {});
2694
3373
  case "stats":
2695
- return /* @__PURE__ */ jsx13(StatsViewer, {});
3374
+ return /* @__PURE__ */ jsx17(StatsViewer, {});
2696
3375
  case "word":
2697
- return /* @__PURE__ */ jsx13(WordLookup, {});
3376
+ return /* @__PURE__ */ jsx17(WordLookup, {});
3377
+ case "help":
3378
+ return /* @__PURE__ */ jsx17(HelpScreen, {});
2698
3379
  }
2699
3380
  }
2700
3381
 
@@ -2716,7 +3397,19 @@ function fmtDuration(ms, lang) {
2716
3397
  return `${m}m ${s}s`;
2717
3398
  }
2718
3399
  function printSessionReport(r, t, lang) {
2719
- if (r.chaptersCompleted === 0) return;
3400
+ if (r.startedAt === null && r.chaptersCompleted === 0) return;
3401
+ if (r.chaptersCompleted === 0) {
3402
+ console.log();
3403
+ console.log(chalk3.bold.cyan(t.report.title));
3404
+ const labelW2 = Math.max(visibleWidth2(t.report.duration), visibleWidth2(t.report.notPracticed)) + 2;
3405
+ const pad2 = (label) => label + " ".repeat(Math.max(0, labelW2 - visibleWidth2(label)));
3406
+ console.log(` ${chalk3.dim(pad2(t.report.duration))} ${fmtDuration(r.totalDurationMs, lang)}`);
3407
+ console.log(` ${chalk3.dim(t.report.notPracticed)}`);
3408
+ console.log();
3409
+ console.log(chalk3.dim(` ${t.report.farewell}`));
3410
+ console.log();
3411
+ return;
3412
+ }
2720
3413
  const accPct = Math.round(r.accuracy * 1e3) / 10;
2721
3414
  const labels = [
2722
3415
  t.report.duration,
@@ -2770,10 +3463,11 @@ async function runPractice(dictIdArg, options) {
2770
3463
  return;
2771
3464
  }
2772
3465
  const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);
3466
+ const stealth = options.stealth === true || cfg.stealth === "default";
2773
3467
  start();
2774
3468
  const { waitUntilExit } = render(
2775
3469
  createElement(App, {
2776
- initial: { name: "practice", params: { dictId, chapterIndex, mode } },
3470
+ initial: { name: "practice", params: { dictId, chapterIndex, mode, stealth } },
2777
3471
  initialCfg: cfg
2778
3472
  }),
2779
3473
  { patchConsole: false, exitOnCtrlC: false }
@@ -2784,9 +3478,11 @@ async function runPractice(dictIdArg, options) {
2784
3478
  printSessionReport(report(), t, lang);
2785
3479
  }
2786
3480
  function buildPracticeCommand() {
2787
- return new Command3("practice").argument("[dictId]", "dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>", "chapter number (1-based)", "1").option("-m, --mode <mode>", "order | dictation | review | random | loop").action(async (dictIdArg, options) => {
2788
- await runPractice(dictIdArg, options);
2789
- });
3481
+ return new Command3("practice").argument("[dictId]", "dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>", "chapter number (1-based)", "1").option("-m, --mode <mode>", "order | dictation | review | random | loop").option("--stealth", "enter stealth mode (minimal UI, no sound)").action(
3482
+ async (dictIdArg, options) => {
3483
+ await runPractice(dictIdArg, options);
3484
+ }
3485
+ );
2790
3486
  }
2791
3487
 
2792
3488
  // src/commands/stats.ts
@@ -2910,6 +3606,16 @@ function buildWordCommand() {
2910
3606
  });
2911
3607
  }
2912
3608
 
3609
+ // src/commands/stealth.ts
3610
+ import { Command as Command6 } from "commander";
3611
+ function buildStealthCommand() {
3612
+ return new Command6("boss").alias("stealth").description("Start practice in stealth mode (minimal UI, looks like plain terminal output)").argument("[dictId]", "dictionary id; falls back to config.defaultDict").option("-c, --chapter <n>", "chapter number (1-based)", "1").option("-m, --mode <mode>", "order | dictation | review | random | loop").action(
3613
+ async (dictIdArg, options) => {
3614
+ await runPractice(dictIdArg, { ...options, stealth: true });
3615
+ }
3616
+ );
3617
+ }
3618
+
2913
3619
  // src/commands/menu.ts
2914
3620
  import { render as render2 } from "ink";
2915
3621
  import { createElement as createElement2 } from "react";
@@ -2931,9 +3637,10 @@ async function runMainMenu() {
2931
3637
  }
2932
3638
 
2933
3639
  // src/cli.ts
2934
- var program = new Command6();
3640
+ var program = new Command7();
2935
3641
  program.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version(package_default.version);
2936
3642
  program.addCommand(buildPracticeCommand());
3643
+ program.addCommand(buildStealthCommand());
2937
3644
  program.addCommand(buildDictCommand());
2938
3645
  program.addCommand(buildWordCommand());
2939
3646
  program.addCommand(buildStatsCommand());