green-screen-react 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -52,6 +52,14 @@ var RestAdapter = class {
52
52
  async setCursor(row, col) {
53
53
  return this.request("POST", "/set-cursor", { row, col });
54
54
  }
55
+ async readMdt(modifiedOnly = true) {
56
+ const qs = modifiedOnly ? "" : "?includeUnmodified=1";
57
+ const resp = await this.request(
58
+ "GET",
59
+ `/read-mdt${qs}`
60
+ );
61
+ return resp?.fields ?? [];
62
+ }
55
63
  async connect(config) {
56
64
  return this.request("POST", "/connect", config);
57
65
  }
@@ -71,9 +79,12 @@ var WebSocketAdapter = class _WebSocketAdapter {
71
79
  this.status = { connected: false, status: "disconnected" };
72
80
  this.pendingScreenResolver = null;
73
81
  this.pendingConnectResolver = null;
82
+ this.pendingMdtResolver = null;
74
83
  this.connectingPromise = null;
75
84
  this.screenListeners = /* @__PURE__ */ new Set();
76
85
  this.statusListeners = /* @__PURE__ */ new Set();
86
+ this.sessionLostListeners = /* @__PURE__ */ new Set();
87
+ this.sessionResumedListeners = /* @__PURE__ */ new Set();
77
88
  this._sessionId = null;
78
89
  this.workerUrl = (options.workerUrl || _WebSocketAdapter.detectEnvUrl() || "http://localhost:3001").replace(/\/+$/, "");
79
90
  }
@@ -108,6 +119,22 @@ var WebSocketAdapter = class _WebSocketAdapter {
108
119
  this.statusListeners.add(listener);
109
120
  return () => this.statusListeners.delete(listener);
110
121
  }
122
+ /**
123
+ * Subscribe to session-lost notifications. Fires when the proxy detects
124
+ * that the server-side session has terminated (host TCP drop, idle
125
+ * timeout, explicit destroy). Integrators use this to prompt a reconnect
126
+ * or swap to a "session expired" UI without relying on error-string
127
+ * matching.
128
+ */
129
+ onSessionLost(listener) {
130
+ this.sessionLostListeners.add(listener);
131
+ return () => this.sessionLostListeners.delete(listener);
132
+ }
133
+ /** Subscribe to session-resumed notifications (fires after a successful `reattach`). */
134
+ onSessionResumed(listener) {
135
+ this.sessionResumedListeners.add(listener);
136
+ return () => this.sessionResumedListeners.delete(listener);
137
+ }
111
138
  async getScreen() {
112
139
  return this.screen;
113
140
  }
@@ -123,6 +150,25 @@ var WebSocketAdapter = class _WebSocketAdapter {
123
150
  async setCursor(row, col) {
124
151
  return this.sendAndWaitForScreen({ type: "setCursor", row, col });
125
152
  }
153
+ async readMdt(modifiedOnly = true) {
154
+ await this.ensureWebSocket();
155
+ return new Promise((resolve) => {
156
+ if (this.pendingMdtResolver) {
157
+ const old = this.pendingMdtResolver;
158
+ this.pendingMdtResolver = null;
159
+ old([]);
160
+ }
161
+ const timeout = setTimeout(() => {
162
+ this.pendingMdtResolver = null;
163
+ resolve([]);
164
+ }, 5e3);
165
+ this.pendingMdtResolver = (fields) => {
166
+ clearTimeout(timeout);
167
+ resolve(fields);
168
+ };
169
+ this.wsSend({ type: "readMdt", modifiedOnly });
170
+ });
171
+ }
126
172
  async connect(config) {
127
173
  await this.ensureWebSocket();
128
174
  if (!config) {
@@ -236,6 +282,22 @@ var WebSocketAdapter = class _WebSocketAdapter {
236
282
  }
237
283
  break;
238
284
  }
285
+ case "mdt": {
286
+ if (this.pendingMdtResolver) {
287
+ const resolver = this.pendingMdtResolver;
288
+ this.pendingMdtResolver = null;
289
+ resolver(msg.data?.fields ?? []);
290
+ }
291
+ break;
292
+ }
293
+ case "session.lost": {
294
+ for (const listener of this.sessionLostListeners) listener(msg.sessionId, msg.status);
295
+ break;
296
+ }
297
+ case "session.resumed": {
298
+ for (const listener of this.sessionResumedListeners) listener(msg.sessionId);
299
+ break;
300
+ }
239
301
  case "status":
240
302
  this.status = msg.data;
241
303
  for (const listener of this.statusListeners) listener(msg.data);
@@ -906,10 +968,15 @@ var RefreshIcon = ({ size = 12, className }) => /* @__PURE__ */ jsxs2("svg", { w
906
968
  /* @__PURE__ */ jsx2("path", { d: "M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" })
907
969
  ] });
908
970
  var KeyIcon = ({ size = 12, style: s }) => /* @__PURE__ */ jsx2("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: s, children: /* @__PURE__ */ jsx2("path", { d: "M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" }) });
909
- var HelpIcon = ({ size = 14 }) => /* @__PURE__ */ jsxs2("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
910
- /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
911
- /* @__PURE__ */ jsx2("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
912
- /* @__PURE__ */ jsx2("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
971
+ var KeyboardIcon = ({ size = 14 }) => /* @__PURE__ */ jsxs2("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
972
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2", ry: "2" }),
973
+ /* @__PURE__ */ jsx2("line", { x1: "6", y1: "10", x2: "6", y2: "10" }),
974
+ /* @__PURE__ */ jsx2("line", { x1: "10", y1: "10", x2: "10", y2: "10" }),
975
+ /* @__PURE__ */ jsx2("line", { x1: "14", y1: "10", x2: "14", y2: "10" }),
976
+ /* @__PURE__ */ jsx2("line", { x1: "18", y1: "10", x2: "18", y2: "10" }),
977
+ /* @__PURE__ */ jsx2("line", { x1: "6", y1: "14", x2: "6", y2: "14" }),
978
+ /* @__PURE__ */ jsx2("line", { x1: "18", y1: "14", x2: "18", y2: "14" }),
979
+ /* @__PURE__ */ jsx2("line", { x1: "10", y1: "14", x2: "14", y2: "14" })
913
980
  ] });
914
981
  var MinimizeIcon = () => /* @__PURE__ */ jsx2("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx2("path", { d: "M4 14h6v6M3 21l7-7M20 10h-6V4M21 3l-7 7" }) });
915
982
 
@@ -1034,8 +1101,201 @@ function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConn
1034
1101
  ] });
1035
1102
  }
1036
1103
 
1104
+ // src/utils/attribute.ts
1105
+ function decodeAttrByte(byte) {
1106
+ const type = byte & 7;
1107
+ const colorGroup = byte & 24;
1108
+ const highIntensity = (byte & 2) !== 0;
1109
+ let color = "green";
1110
+ switch (colorGroup) {
1111
+ case 0:
1112
+ color = highIntensity ? "white" : "green";
1113
+ break;
1114
+ case 8:
1115
+ color = "red";
1116
+ break;
1117
+ // red stays red at HI
1118
+ case 16:
1119
+ color = highIntensity ? "yellow" : "turquoise";
1120
+ break;
1121
+ case 24:
1122
+ color = highIntensity ? "blue" : "pink";
1123
+ break;
1124
+ }
1125
+ const nonDisplay = type === 7;
1126
+ const underscore = type === 4 || type === 5 || type === 6;
1127
+ const reverse = type === 5;
1128
+ const columnSeparator = type === 1 || type === 3;
1129
+ const hiTypeBit = type === 2 || type === 3 || type === 6;
1130
+ return {
1131
+ color,
1132
+ highIntensity: highIntensity || hiTypeBit,
1133
+ underscore,
1134
+ reverse,
1135
+ blink: false,
1136
+ // 5250 has no base-attr blink; blink comes from WEA highlight
1137
+ nonDisplay,
1138
+ columnSeparator
1139
+ };
1140
+ }
1141
+ function decodeExtColor(byte) {
1142
+ switch (byte & 15) {
1143
+ case 0:
1144
+ return "green";
1145
+ case 1:
1146
+ return "blue";
1147
+ case 2:
1148
+ return "red";
1149
+ case 3:
1150
+ return "pink";
1151
+ case 4:
1152
+ return "turquoise";
1153
+ case 5:
1154
+ return "yellow";
1155
+ case 6:
1156
+ return "white";
1157
+ case 7:
1158
+ return void 0;
1159
+ // default — inherit
1160
+ // Reverse-image pairs: distinct tokens so themes can override
1161
+ case 8:
1162
+ return "green";
1163
+ // reverse green → typically same color, background flip
1164
+ case 9:
1165
+ return "blue";
1166
+ case 10:
1167
+ return "red";
1168
+ case 11:
1169
+ return "pink";
1170
+ case 12:
1171
+ return "turquoise";
1172
+ case 13:
1173
+ return "yellow";
1174
+ case 14:
1175
+ return "white";
1176
+ case 15:
1177
+ return void 0;
1178
+ default:
1179
+ return void 0;
1180
+ }
1181
+ }
1182
+ function extColorIsReverse(byte) {
1183
+ const v = byte & 15;
1184
+ return v >= 8 && v <= 14;
1185
+ }
1186
+ function decodeExtHighlight(byte) {
1187
+ return {
1188
+ underscore: (byte & 1) !== 0,
1189
+ reverse: (byte & 2) !== 0,
1190
+ blink: (byte & 4) !== 0,
1191
+ columnSeparator: (byte & 8) !== 0
1192
+ };
1193
+ }
1194
+ function cssVarForColor(color) {
1195
+ if (!color) return "var(--gs-green, #10b981)";
1196
+ return `var(--gs-${color}, var(--gs-green, #10b981))`;
1197
+ }
1198
+
1199
+ // src/utils/validation.ts
1200
+ function filterFieldChar(field, ch, isLastPosition = false) {
1201
+ let out = ch;
1202
+ if (field.monocase) {
1203
+ out = out.toUpperCase();
1204
+ }
1205
+ const cp = out.charCodeAt(0);
1206
+ const isAsciiLetter = cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122;
1207
+ const isAsciiDigit = cp >= 48 && cp <= 57;
1208
+ switch (field.shift_type) {
1209
+ case "alpha_only": {
1210
+ if (isAsciiLetter || out === "," || out === "-" || out === "." || out === " ") return out;
1211
+ return null;
1212
+ }
1213
+ case "numeric_only": {
1214
+ if (isAsciiDigit || out === "-" || out === "+" || out === "," || out === "." || out === " ") return out;
1215
+ return null;
1216
+ }
1217
+ case "digits_only": {
1218
+ if (isAsciiDigit) return out;
1219
+ return null;
1220
+ }
1221
+ case "signed_num": {
1222
+ if (isAsciiDigit) return out;
1223
+ if (isLastPosition && (out === "-" || out === "+")) return out;
1224
+ return null;
1225
+ }
1226
+ case "katakana": {
1227
+ if (cp >= 65377 && cp <= 65439 || isAsciiDigit || out === " ") return out;
1228
+ return null;
1229
+ }
1230
+ case "alpha":
1231
+ case "numeric_shift":
1232
+ case "io":
1233
+ case void 0:
1234
+ default:
1235
+ return out;
1236
+ }
1237
+ }
1238
+ function filterFieldInput(field, input, startOffset) {
1239
+ let out = "";
1240
+ let rejected = false;
1241
+ for (let i = 0; i < input.length; i++) {
1242
+ const pos = startOffset + i;
1243
+ const isLast = pos === field.length - 1;
1244
+ const transformed = filterFieldChar(field, input[i], isLast);
1245
+ if (transformed === null) {
1246
+ rejected = true;
1247
+ } else {
1248
+ out += transformed;
1249
+ }
1250
+ }
1251
+ return { out, rejected };
1252
+ }
1253
+ function validateMod10(value) {
1254
+ const digits = value.replace(/\D/g, "");
1255
+ if (digits.length < 2) return true;
1256
+ let sum = 0;
1257
+ let alt = false;
1258
+ for (let i = digits.length - 1; i >= 0; i--) {
1259
+ let d = digits.charCodeAt(i) - 48;
1260
+ if (alt) {
1261
+ d *= 2;
1262
+ if (d > 9) d -= 9;
1263
+ }
1264
+ sum += d;
1265
+ alt = !alt;
1266
+ }
1267
+ return sum % 10 === 0;
1268
+ }
1269
+ function validateMod11(value) {
1270
+ const digits = value.replace(/\D/g, "");
1271
+ if (digits.length < 2) return true;
1272
+ const body = digits.slice(0, -1);
1273
+ const check = digits.charCodeAt(digits.length - 1) - 48;
1274
+ let sum = 0;
1275
+ let weight = 2;
1276
+ for (let i = body.length - 1; i >= 0; i--) {
1277
+ sum += (body.charCodeAt(i) - 48) * weight;
1278
+ weight++;
1279
+ if (weight > 7) weight = 2;
1280
+ }
1281
+ const remainder = sum % 11;
1282
+ const expected = remainder === 0 ? 0 : 11 - remainder;
1283
+ return expected === check;
1284
+ }
1285
+
1037
1286
  // src/components/GreenScreenTerminal.tsx
1038
1287
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1288
+ function aidByteToKeyName(aid) {
1289
+ if (aid === 241) return "ENTER";
1290
+ if (aid === 243) return "HELP";
1291
+ if (aid === 244) return "PAGEUP";
1292
+ if (aid === 245) return "PAGEDOWN";
1293
+ if (aid === 246) return "PRINT";
1294
+ if (aid === 189) return "CLEAR";
1295
+ if (aid >= 49 && aid <= 60) return `F${aid - 48}`;
1296
+ if (aid >= 177 && aid <= 188) return `F${aid - 176 + 12}`;
1297
+ return null;
1298
+ }
1039
1299
  var noopResult = { success: false, error: "No adapter configured" };
1040
1300
  var noopAdapter = {
1041
1301
  getScreen: async () => null,
@@ -1066,14 +1326,16 @@ function GreenScreenTerminal({
1066
1326
  inlineSignIn = true,
1067
1327
  defaultProtocol: signInDefaultProtocol,
1068
1328
  onSignIn,
1069
- autoSignedIn,
1070
1329
  autoFocusDisabled = false,
1071
1330
  bootLoader,
1331
+ bootLoaderReady,
1072
1332
  headerRight,
1333
+ statusActions,
1073
1334
  overlay,
1074
1335
  onNotification,
1075
1336
  onScreenChange,
1076
1337
  onMinimize,
1338
+ showShortcutsButton = true,
1077
1339
  className,
1078
1340
  style
1079
1341
  }) {
@@ -1118,22 +1380,17 @@ function GreenScreenTerminal({
1118
1380
  const sendText = useCallback3(async (text) => _sendText(text), [_sendText]);
1119
1381
  const sendKey = useCallback3(async (key) => _sendKey(key), [_sendKey]);
1120
1382
  const [optimisticEdits, setOptimisticEdits] = useState5([]);
1121
- const prevScreenSigForEdits = useRef4(void 0);
1383
+ const prevScreenContentForEdits = useRef4(void 0);
1122
1384
  useEffect4(() => {
1123
- if (rawScreenData?.screen_signature && rawScreenData.screen_signature !== prevScreenSigForEdits.current) {
1124
- prevScreenSigForEdits.current = rawScreenData.screen_signature;
1385
+ const content = rawScreenData?.content;
1386
+ if (content && content !== prevScreenContentForEdits.current) {
1387
+ prevScreenContentForEdits.current = content;
1125
1388
  setOptimisticEdits([]);
1126
1389
  }
1127
- }, [rawScreenData?.screen_signature]);
1390
+ }, [rawScreenData?.content]);
1128
1391
  const [inputText, setInputText] = useState5("");
1129
1392
  const [isFocused, setIsFocused] = useState5(false);
1130
- const [showSignInHint, setShowSignInHint] = useState5(false);
1131
1393
  const [showShortcuts, setShowShortcuts] = useState5(false);
1132
- const prevAutoSignedIn = useRef4(false);
1133
- useEffect4(() => {
1134
- if (autoSignedIn && !prevAutoSignedIn.current) setShowSignInHint(true);
1135
- prevAutoSignedIn.current = !!autoSignedIn;
1136
- }, [autoSignedIn]);
1137
1394
  const terminalRef = useRef4(null);
1138
1395
  const inputRef = useRef4(null);
1139
1396
  const [syncedCursor, setSyncedCursor] = useState5(null);
@@ -1143,7 +1400,6 @@ function GreenScreenTerminal({
1143
1400
  if (prevRawContentRef.current && newContent && newContent !== prevRawContentRef.current) {
1144
1401
  setSyncedCursor(null);
1145
1402
  setInputText("");
1146
- if (showSignInHint) setShowSignInHint(false);
1147
1403
  }
1148
1404
  prevRawContentRef.current = newContent;
1149
1405
  }, [rawScreenData?.content]);
@@ -1206,11 +1462,9 @@ function GreenScreenTerminal({
1206
1462
  const newAdapter = new RestAdapter({ baseUrl: `http://${config.host}${port}` });
1207
1463
  setInternalAdapter(newAdapter);
1208
1464
  await newAdapter.connect(config);
1209
- if (config.username && config.password) setShowSignInHint(true);
1210
1465
  return;
1211
1466
  }
1212
1467
  await connect(config);
1213
- if (config.username && config.password) setShowSignInHint(true);
1214
1468
  } catch (err) {
1215
1469
  setSignInError(err instanceof Error ? err.message : String(err));
1216
1470
  setConnecting(false);
@@ -1222,13 +1476,15 @@ function GreenScreenTerminal({
1222
1476
  const [showBootLoader, setShowBootLoader] = useState5(bootLoader !== false);
1223
1477
  const [bootFadingOut, setBootFadingOut] = useState5(false);
1224
1478
  useEffect4(() => {
1225
- if (screenData?.content && showBootLoader) {
1479
+ if (!showBootLoader) return;
1480
+ const shouldDismiss = bootLoaderReady !== void 0 ? bootLoaderReady : !!screenData?.content;
1481
+ if (shouldDismiss) {
1226
1482
  setBootFadingOut(true);
1227
1483
  setShowBootLoader(false);
1228
1484
  const timer = setTimeout(() => setBootFadingOut(false), 400);
1229
1485
  return () => clearTimeout(timer);
1230
1486
  }
1231
- }, [screenData?.content, showBootLoader]);
1487
+ }, [screenData?.content, showBootLoader, bootLoaderReady]);
1232
1488
  const FOCUS_STORAGE_KEY = "gs-terminal-focused";
1233
1489
  useEffect4(() => {
1234
1490
  if (!autoFocusDisabled && !readOnly) {
@@ -1296,13 +1552,25 @@ function GreenScreenTerminal({
1296
1552
  const clickedRow = Math.floor(y / ROW_HEIGHT);
1297
1553
  const clickedCol = Math.floor(x / charWidth);
1298
1554
  if (clickedRow < 0 || clickedRow >= (screenData.rows || 24) || clickedCol < 0 || clickedCol >= (screenData.cols || 80)) return;
1555
+ const clickedField = screenData.fields.find(
1556
+ (f) => f.row === clickedRow && clickedCol >= f.col && clickedCol < f.col + f.length
1557
+ );
1558
+ const ptrAid = clickedField && clickedField.pointer_aid;
1559
+ if (ptrAid) {
1560
+ const keyName = aidByteToKeyName(ptrAid);
1561
+ if (keyName) {
1562
+ setSyncedCursor({ row: clickedRow, col: clickedCol });
1563
+ adapter.setCursor?.(clickedRow, clickedCol).then(() => sendKey(keyName));
1564
+ return;
1565
+ }
1566
+ }
1299
1567
  setSyncedCursor({ row: clickedRow, col: clickedCol });
1300
1568
  adapter.setCursor?.(clickedRow, clickedCol).then((r) => {
1301
1569
  if (r?.cursor_row !== void 0) {
1302
1570
  setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1303
1571
  }
1304
1572
  });
1305
- }, [readOnly, screenData, adapter]);
1573
+ }, [readOnly, screenData, adapter, sendKey]);
1306
1574
  const getCurrentField = useCallback3(() => {
1307
1575
  const fields = screenData?.fields || [];
1308
1576
  const cursorRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
@@ -1312,6 +1580,31 @@ function GreenScreenTerminal({
1312
1580
  }
1313
1581
  return null;
1314
1582
  }, [screenData, syncedCursor]);
1583
+ const readFieldValue = useCallback3((field) => {
1584
+ if (!screenData?.content) return "";
1585
+ const lines = screenData.content.split("\n");
1586
+ const line = lines[field.row] || "";
1587
+ return line.substring(field.col, field.col + field.length).replace(/\s+$/, "");
1588
+ }, [screenData?.content]);
1589
+ const [validationError, setValidationError] = useState5(null);
1590
+ const runSelfCheck = useCallback3(() => {
1591
+ const fields = screenData?.fields || [];
1592
+ for (const f of fields) {
1593
+ if (!f.is_input) continue;
1594
+ const val = readFieldValue(f);
1595
+ if (!val) continue;
1596
+ if (f.self_check_mod10 && !validateMod10(val)) {
1597
+ setValidationError(`Invalid check digit (MOD10) in field at row ${f.row + 1}, col ${f.col + 1}`);
1598
+ return false;
1599
+ }
1600
+ if (f.self_check_mod11 && !validateMod11(val)) {
1601
+ setValidationError(`Invalid check digit (MOD11) in field at row ${f.row + 1}, col ${f.col + 1}`);
1602
+ return false;
1603
+ }
1604
+ }
1605
+ setValidationError(null);
1606
+ return true;
1607
+ }, [screenData?.fields, readFieldValue]);
1315
1608
  const handleKeyDown = async (e) => {
1316
1609
  if (readOnly) {
1317
1610
  e.preventDefault();
@@ -1350,18 +1643,19 @@ function GreenScreenTerminal({
1350
1643
  End: "END",
1351
1644
  Insert: "INSERT"
1352
1645
  };
1353
- if (e.key.startsWith("F") && e.key.length <= 3) {
1646
+ if (/^F([1-9]|1[0-9]|2[0-4])$/.test(e.key)) {
1354
1647
  e.preventDefault();
1355
- const fKey = e.key.toUpperCase();
1356
- if (/^F([1-9]|1[0-9]|2[0-4])$/.test(fKey)) {
1357
- const kr = await sendKey(fKey);
1358
- if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1359
- return;
1360
- }
1648
+ if (!runSelfCheck()) return;
1649
+ const kr = await sendKey(e.key);
1650
+ if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1651
+ return;
1361
1652
  }
1362
1653
  if (keyMap[e.key]) {
1363
1654
  e.preventDefault();
1364
- const kr = await sendKey(keyMap[e.key]);
1655
+ const k = keyMap[e.key];
1656
+ const isSubmit = k === "ENTER" || k === "PAGEUP" || k === "PAGEDOWN";
1657
+ if (isSubmit && !runSelfCheck()) return;
1658
+ const kr = await sendKey(k);
1365
1659
  if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1366
1660
  }
1367
1661
  };
@@ -1372,18 +1666,32 @@ function GreenScreenTerminal({
1372
1666
  }
1373
1667
  const newText = e.target.value;
1374
1668
  if (newText.length > inputText.length) {
1375
- const newChars = newText.substring(inputText.length);
1376
- const curRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
1377
- const curCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1378
- const edits = [];
1379
- for (let i = 0; i < newChars.length; i++) {
1380
- edits.push({ row: curRow, col: curCol + i, ch: newChars[i] });
1669
+ let newChars = newText.substring(inputText.length);
1670
+ const curField = getCurrentField();
1671
+ if (curField && (curField.shift_type || curField.monocase)) {
1672
+ const curColAbs = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1673
+ const startOffset = Math.max(0, curColAbs - curField.col);
1674
+ const { out, rejected } = filterFieldInput(curField, newChars, startOffset);
1675
+ if (rejected) {
1676
+ const what = curField.shift_type === "digits_only" || curField.shift_type === "numeric_only" || curField.shift_type === "signed_num" ? "digits only" : curField.shift_type === "alpha_only" ? "letters only" : curField.shift_type === "katakana" ? "katakana only" : "character not allowed";
1677
+ setValidationError(`Field accepts ${what}`);
1678
+ setTimeout(() => setValidationError(null), 1500);
1679
+ }
1680
+ newChars = out;
1681
+ }
1682
+ if (newChars.length > 0) {
1683
+ const curRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
1684
+ const curCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1685
+ const edits = [];
1686
+ for (let i = 0; i < newChars.length; i++) {
1687
+ edits.push({ row: curRow, col: curCol + i, ch: newChars[i] });
1688
+ }
1689
+ setOptimisticEdits((prev) => [...prev, ...edits]);
1690
+ setSyncedCursor({ row: curRow, col: curCol + newChars.length });
1691
+ sendText(newChars).then((r) => {
1692
+ if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1693
+ });
1381
1694
  }
1382
- setOptimisticEdits((prev) => [...prev, ...edits]);
1383
- setSyncedCursor({ row: curRow, col: curCol + newChars.length });
1384
- sendText(newChars).then((r) => {
1385
- if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1386
- });
1387
1695
  }
1388
1696
  setInputText("");
1389
1697
  e.target.value = "";
@@ -1415,41 +1723,156 @@ function GreenScreenTerminal({
1415
1723
  if (lastIndex < text.length) segments.push(/* @__PURE__ */ jsx4("span", { children: text.substring(lastIndex) }, `${keyPrefix}-e`));
1416
1724
  return segments.length > 0 ? /* @__PURE__ */ jsx4(Fragment, { children: segments }) : /* @__PURE__ */ jsx4(Fragment, { children: text });
1417
1725
  }, []);
1418
- const renderRowWithFields = useCallback3((line, rowIndex, fields) => {
1726
+ const renderRowWithFields = useCallback3((line, rowIndex, fields, cursorRow, cursorCol) => {
1419
1727
  const inputFields = fields.filter((f) => f.row === rowIndex && f.is_input);
1420
1728
  const highlightedFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_highlighted);
1421
1729
  const reverseFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_reverse);
1422
1730
  const colorFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.color && !f.is_highlighted && !f.is_reverse);
1423
1731
  const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields, ...colorFields];
1424
- if (allRowFields.length === 0) return /* @__PURE__ */ jsx4("span", { children: line });
1732
+ const cols = screenData?.cols || profile.defaultCols;
1733
+ const extAttrs = screenData?.ext_attrs;
1734
+ const dbcsCont = screenData?.dbcs_cont;
1735
+ const dbcsContSet = dbcsCont && dbcsCont.length > 0 ? new Set(dbcsCont) : null;
1736
+ const hasExtOnRow = !!extAttrs && (() => {
1737
+ for (let c = 0; c < cols; c++) {
1738
+ if (extAttrs[rowIndex * cols + c]) return true;
1739
+ }
1740
+ return false;
1741
+ })();
1742
+ const hasDbcsOnRow = !!dbcsContSet && (() => {
1743
+ for (let c = 0; c < cols; c++) {
1744
+ if (dbcsContSet.has(rowIndex * cols + c)) return true;
1745
+ }
1746
+ return false;
1747
+ })();
1748
+ if (allRowFields.length === 0 && !hasExtOnRow && !hasDbcsOnRow) return /* @__PURE__ */ jsx4("span", { children: line });
1425
1749
  const sorted = [...allRowFields].sort((a, b) => a.col - b.col);
1426
1750
  const segs = [];
1427
1751
  let lastEnd = 0;
1428
- const cols = screenData?.cols || profile.defaultCols;
1752
+ const renderPlainRun = (runStart, runEnd, keyPrefix) => {
1753
+ if (runStart >= runEnd) return;
1754
+ if (!hasExtOnRow && !hasDbcsOnRow) {
1755
+ segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(runStart, runEnd) }, keyPrefix));
1756
+ return;
1757
+ }
1758
+ let pos = runStart;
1759
+ while (pos < runEnd) {
1760
+ const addr = rowIndex * cols + pos;
1761
+ const ext = extAttrs?.[addr];
1762
+ const isContCell = dbcsContSet?.has(addr);
1763
+ if (isContCell) {
1764
+ segs.push(
1765
+ /* @__PURE__ */ jsx4(
1766
+ "span",
1767
+ {
1768
+ style: { display: "inline-block", width: "1ch" },
1769
+ "aria-hidden": "true"
1770
+ },
1771
+ `${keyPrefix}-dc${pos}`
1772
+ )
1773
+ );
1774
+ pos++;
1775
+ continue;
1776
+ }
1777
+ if (ext) {
1778
+ const color = ext.color !== void 0 ? decodeExtColor(ext.color) : void 0;
1779
+ const colorReverse = ext.color !== void 0 && extColorIsReverse(ext.color);
1780
+ const hl = ext.highlight !== void 0 ? decodeExtHighlight(ext.highlight) : void 0;
1781
+ const style2 = {};
1782
+ if (color) style2.color = cssVarForColor(color);
1783
+ if (colorReverse) {
1784
+ style2.background = cssVarForColor(color);
1785
+ style2.color = "#000";
1786
+ }
1787
+ if (hl?.reverse) {
1788
+ style2.background = "currentColor";
1789
+ style2.color = "#000";
1790
+ }
1791
+ if (hl?.underscore) style2.textDecoration = "underline";
1792
+ if (hl?.blink) style2.animation = "gs-blink 1s steps(2, start) infinite";
1793
+ segs.push(
1794
+ /* @__PURE__ */ jsx4("span", { style: style2, children: line[pos] }, `${keyPrefix}-x${pos}`)
1795
+ );
1796
+ pos++;
1797
+ continue;
1798
+ }
1799
+ let runEndPlain = pos + 1;
1800
+ while (runEndPlain < runEnd) {
1801
+ const a = rowIndex * cols + runEndPlain;
1802
+ if (extAttrs?.[a] || dbcsContSet?.has(a)) break;
1803
+ runEndPlain++;
1804
+ }
1805
+ segs.push(
1806
+ /* @__PURE__ */ jsx4("span", { children: line.substring(pos, runEndPlain) }, `${keyPrefix}-p${pos}`)
1807
+ );
1808
+ pos = runEndPlain;
1809
+ }
1810
+ };
1429
1811
  sorted.forEach((field, idx) => {
1430
1812
  const fs = field.col;
1431
1813
  const fe = Math.min(field.col + field.length, cols);
1432
- if (fs > lastEnd) segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(lastEnd, fs) }, `t${idx}`));
1814
+ if (fs > lastEnd) renderPlainRun(lastEnd, fs, `t${idx}`);
1433
1815
  const fc = line.substring(fs, fe);
1434
- const colorVar = field.color ? `var(--gs-${field.color}, var(--gs-green))` : void 0;
1816
+ const cursorInField = cursorRow === field.row && cursorCol >= fs && cursorCol < field.col + field.length;
1817
+ const entryAttr = cursorInField && field.highlight_entry_attr !== void 0 ? decodeAttrByte(field.highlight_entry_attr) : null;
1818
+ const baseColor = entryAttr?.color ?? field.color;
1819
+ const colorVar = baseColor ? cssVarForColor(baseColor) : void 0;
1820
+ const fieldStyle = {};
1821
+ if (colorVar) fieldStyle.color = colorVar;
1822
+ if (entryAttr?.reverse) {
1823
+ fieldStyle.background = "currentColor";
1824
+ fieldStyle.color = "#000";
1825
+ }
1826
+ if (entryAttr?.underscore) fieldStyle.textDecoration = "underline";
1827
+ if (entryAttr?.highIntensity) fieldStyle.fontWeight = "bold";
1828
+ const isPassword = field.is_non_display;
1829
+ const displayText = isPassword ? " ".repeat(fc.length) : fc;
1435
1830
  if (field.is_input) {
1436
1831
  const fieldWidth = Math.min(field.length, cols - fs);
1437
- const fieldClass = showSignInHint ? "gs-confirmed-field" : field.is_underscored ? "gs-input-field" : void 0;
1438
- segs.push(/* @__PURE__ */ jsx4("span", { className: fieldClass || void 0, style: { display: "inline-block", width: `${fieldWidth}ch`, overflow: "hidden", color: colorVar }, children: fc }, `f${idx}`));
1832
+ const fieldClass = field.is_underscored ? "gs-input-field" : void 0;
1833
+ const extra = [];
1834
+ if (field.is_dbcs) extra.push("gs-dbcs-field");
1835
+ if (cursorInField) extra.push("gs-field-active");
1836
+ const composedClass = [fieldClass, ...extra].filter(Boolean).join(" ") || void 0;
1837
+ segs.push(
1838
+ /* @__PURE__ */ jsx4(
1839
+ "span",
1840
+ {
1841
+ className: composedClass,
1842
+ style: {
1843
+ display: "inline-block",
1844
+ width: `${fieldWidth}ch`,
1845
+ overflow: "hidden",
1846
+ ...fieldStyle
1847
+ },
1848
+ children: displayText
1849
+ },
1850
+ `f${idx}`
1851
+ )
1852
+ );
1439
1853
  } else if (field.is_reverse) {
1440
- segs.push(/* @__PURE__ */ jsx4("span", { style: { color: colorVar || "var(--gs-red, #FF5555)", fontWeight: "bold" }, children: fc }, `v${idx}`));
1441
- } else if (colorVar) {
1442
- segs.push(/* @__PURE__ */ jsx4("span", { style: { color: colorVar }, children: fc }, `h${idx}`));
1854
+ segs.push(
1855
+ /* @__PURE__ */ jsx4(
1856
+ "span",
1857
+ {
1858
+ style: { color: colorVar || "var(--gs-red, #FF5555)", fontWeight: "bold", ...fieldStyle },
1859
+ children: displayText
1860
+ },
1861
+ `v${idx}`
1862
+ )
1863
+ );
1864
+ } else if (colorVar || entryAttr) {
1865
+ segs.push(/* @__PURE__ */ jsx4("span", { style: fieldStyle, children: displayText }, `h${idx}`));
1443
1866
  } else if (field.is_highlighted) {
1444
- segs.push(/* @__PURE__ */ jsx4("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: fc }, `h${idx}`));
1867
+ segs.push(/* @__PURE__ */ jsx4("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: displayText }, `h${idx}`));
1445
1868
  } else {
1446
- segs.push(/* @__PURE__ */ jsx4("span", { children: fc }, `h${idx}`));
1869
+ segs.push(/* @__PURE__ */ jsx4("span", { children: displayText }, `h${idx}`));
1447
1870
  }
1448
1871
  lastEnd = fe;
1449
1872
  });
1450
- if (lastEnd < line.length) segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(lastEnd) }, "te"));
1873
+ if (lastEnd < line.length) renderPlainRun(lastEnd, line.length, "te");
1451
1874
  return /* @__PURE__ */ jsx4(Fragment, { children: segs });
1452
- }, [renderTextWithUnderlines, screenData?.cols, profile.defaultCols, showSignInHint]);
1875
+ }, [renderTextWithUnderlines, screenData, profile.defaultCols]);
1453
1876
  const renderScreen = () => {
1454
1877
  if (showBootLoader && !screenData?.content) {
1455
1878
  if (bootLoader === false) return null;
@@ -1463,10 +1886,10 @@ function GreenScreenTerminal({
1463
1886
  }
1464
1887
  return /* @__PURE__ */ jsx4("div", { style: { width: `${screenData?.cols || profile.defaultCols}ch`, height: `${(screenData?.rows || profile.defaultRows) * 21}px`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxs4("div", { style: { textAlign: "center" }, children: [
1465
1888
  /* @__PURE__ */ jsx4("div", { style: { color: "#808080", marginBottom: "12px" }, children: /* @__PURE__ */ jsx4(TerminalIcon, { size: 40 }) }),
1466
- /* @__PURE__ */ jsx4("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : "Not connected" }),
1889
+ /* @__PURE__ */ jsx4("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: connStatus?.status === "connecting" ? "#f59e0b" : connStatus?.status === "loading" ? "#94a3b8" : "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : connStatus?.status === "connecting" ? "Connecting..." : connStatus?.status === "loading" ? "Loading..." : "Not connected" }),
1467
1890
  !connStatus?.connected && isUsingDefaultAdapter && /* @__PURE__ */ jsxs4("p", { style: { fontFamily: "var(--gs-font)", fontSize: "11px", color: "#606060", marginTop: "8px" }, children: [
1468
1891
  "Start the proxy: ",
1469
- /* @__PURE__ */ jsx4("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy --mock" })
1892
+ /* @__PURE__ */ jsx4("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy" })
1470
1893
  ] })
1471
1894
  ] }) });
1472
1895
  }
@@ -1484,7 +1907,7 @@ function GreenScreenTerminal({
1484
1907
  const cursor = getCursorPos();
1485
1908
  const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
1486
1909
  const cursorInInputField = hasCursor && fields.some(
1487
- (f) => f.is_input && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
1910
+ (f) => f.is_input && !f.is_non_display && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
1488
1911
  );
1489
1912
  return /* @__PURE__ */ jsxs4("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
1490
1913
  rows.map((line, index) => {
@@ -1499,11 +1922,33 @@ function GreenScreenTerminal({
1499
1922
  }
1500
1923
  const headerSegments = index === 0 ? profile.colors.parseHeaderRow(displayLine) : null;
1501
1924
  return /* @__PURE__ */ jsxs4("div", { className: headerSegments ? "" : profile.colors.getRowColorClass(index, displayLine, termRows), style: { height: `${ROW_HEIGHT}px`, lineHeight: `${ROW_HEIGHT}px`, whiteSpace: "pre", position: "relative" }, children: [
1502
- headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ jsx4("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields),
1503
- cursorInInputField && !showSignInHint && index === cursor.row && /* @__PURE__ */ jsx4("span", { className: "gs-cursor", style: { position: "absolute", left: `${cursor.col}ch`, width: "1ch", height: `${ROW_HEIGHT}px`, top: 0, pointerEvents: "none" } })
1925
+ headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ jsx4("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields, cursor.row, cursor.col),
1926
+ cursorInInputField && index === cursor.row && /* @__PURE__ */ jsx4("span", { className: "gs-cursor", style: { position: "absolute", left: `${cursor.col}ch`, width: "1ch", height: `${ROW_HEIGHT}px`, top: 0, pointerEvents: "none" } })
1504
1927
  ] }, index);
1505
1928
  }),
1506
- showSignInHint && /* @__PURE__ */ jsx4("div", { className: "gs-signin-hint", children: "Signed in \u2014 press Enter to continue" }),
1929
+ validationError && /* @__PURE__ */ jsxs4(
1930
+ "div",
1931
+ {
1932
+ role: "alert",
1933
+ style: {
1934
+ position: "absolute",
1935
+ left: 0,
1936
+ right: 0,
1937
+ bottom: 0,
1938
+ padding: "2px 6px",
1939
+ background: "var(--gs-red, #FF5555)",
1940
+ color: "#000",
1941
+ fontFamily: "var(--gs-font)",
1942
+ fontSize: "12px",
1943
+ zIndex: 10
1944
+ },
1945
+ onClick: () => setValidationError(null),
1946
+ children: [
1947
+ validationError,
1948
+ " \u2014 click to dismiss"
1949
+ ]
1950
+ }
1951
+ ),
1507
1952
  screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0 && /* @__PURE__ */ jsxs4("span", { style: { position: "absolute", bottom: 0, right: 0, fontFamily: "var(--gs-font)", fontSize: "10px", color: "var(--gs-green, #10b981)", pointerEvents: "none", opacity: 0.6 }, children: [
1508
1953
  String(screenData.cursor_row + 1).padStart(2, "0"),
1509
1954
  "/",
@@ -1534,6 +1979,15 @@ function GreenScreenTerminal({
1534
1979
  return "#64748b";
1535
1980
  }
1536
1981
  };
1982
+ const handleShortcutSend = useCallback3(async (key) => {
1983
+ if (readOnly) return;
1984
+ const isSubmit = key === "ENTER" || key === "PAGEUP" || key === "PAGEDOWN" || /^F([1-9]|1[0-9]|2[0-4])$/.test(key);
1985
+ if (isSubmit && !runSelfCheck()) return;
1986
+ setIsFocused(true);
1987
+ inputRef.current?.focus();
1988
+ const kr = await sendKey(key);
1989
+ if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1990
+ }, [readOnly, runSelfCheck, sendKey]);
1537
1991
  return /* @__PURE__ */ jsxs4("div", { className: `gs-terminal ${isFocused ? "gs-terminal-focused" : ""} ${className || ""}`, style, children: [
1538
1992
  showHeader && /* @__PURE__ */ jsx4("div", { className: "gs-header", children: embedded ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1539
1993
  /* @__PURE__ */ jsxs4("span", { className: "gs-header-left", children: [
@@ -1546,16 +2000,17 @@ function GreenScreenTerminal({
1546
2000
  screenData?.insert_mode && /* @__PURE__ */ jsx4("span", { className: "gs-badge-ins", children: "INS" })
1547
2001
  ] }),
1548
2002
  /* @__PURE__ */ jsxs4("div", { className: "gs-header-right", children: [
1549
- connStatus?.status && /* @__PURE__ */ jsx4(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
1550
- connStatus && (connStatus.connected ? /* @__PURE__ */ jsx4(WifiIcon, { size: 12, style: { color: "var(--gs-green, #10b981)" } }) : /* @__PURE__ */ jsx4(WifiOffIcon, { size: 12, style: { color: "#FF6B00" } })),
2003
+ connStatus?.status && connStatus.status !== "loading" && /* @__PURE__ */ jsx4(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
2004
+ connStatus && connStatus.status !== "loading" && (connStatus.connected ? /* @__PURE__ */ jsx4(WifiIcon, { size: 12, style: { color: "var(--gs-green, #10b981)" } }) : /* @__PURE__ */ jsx4(WifiOffIcon, { size: 12, style: { color: "#FF6B00" } })),
2005
+ statusActions,
1551
2006
  onMinimize && /* @__PURE__ */ jsx4("button", { onClick: (e) => {
1552
2007
  e.stopPropagation();
1553
2008
  onMinimize();
1554
2009
  }, className: "gs-btn-icon", title: "Minimize terminal", children: /* @__PURE__ */ jsx4(MinimizeIcon, {}) }),
1555
- /* @__PURE__ */ jsx4("button", { onClick: (e) => {
2010
+ showShortcutsButton && /* @__PURE__ */ jsx4("button", { onClick: (e) => {
1556
2011
  e.stopPropagation();
1557
2012
  setShowShortcuts((s) => !s);
1558
- }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ jsx4(HelpIcon, { size: 12 }) }),
2013
+ }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ jsx4(KeyboardIcon, { size: 12 }) }),
1559
2014
  headerRight
1560
2015
  ] })
1561
2016
  ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
@@ -1569,7 +2024,7 @@ function GreenScreenTerminal({
1569
2024
  screenData?.insert_mode && /* @__PURE__ */ jsx4("span", { className: "gs-badge-ins", children: "INS" })
1570
2025
  ] }),
1571
2026
  /* @__PURE__ */ jsxs4("div", { className: "gs-header-right", children: [
1572
- connStatus && /* @__PURE__ */ jsx4("div", { className: "gs-status-group", children: connStatus.connected ? /* @__PURE__ */ jsxs4(Fragment, { children: [
2027
+ connStatus && connStatus.status !== "loading" && /* @__PURE__ */ jsx4("div", { className: "gs-status-group", children: connStatus.connected ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1573
2028
  /* @__PURE__ */ jsx4(WifiIcon, { size: 12, style: { color: "var(--gs-green, #10b981)" } }),
1574
2029
  /* @__PURE__ */ jsx4("span", { className: "gs-host", children: connStatus.host })
1575
2030
  ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
@@ -1581,10 +2036,11 @@ function GreenScreenTerminal({
1581
2036
  /* @__PURE__ */ jsx4(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
1582
2037
  connStatus.username && /* @__PURE__ */ jsx4("span", { className: "gs-host", children: connStatus.username })
1583
2038
  ] }),
1584
- /* @__PURE__ */ jsx4("button", { onClick: (e) => {
2039
+ statusActions,
2040
+ showShortcutsButton && /* @__PURE__ */ jsx4("button", { onClick: (e) => {
1585
2041
  e.stopPropagation();
1586
2042
  setShowShortcuts((s) => !s);
1587
- }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ jsx4(HelpIcon, { size: 12 }) }),
2043
+ }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ jsx4(KeyboardIcon, { size: 12 }) }),
1588
2044
  headerRight
1589
2045
  ] })
1590
2046
  ] }) }),
@@ -1605,29 +2061,47 @@ function GreenScreenTerminal({
1605
2061
  showShortcuts && /* @__PURE__ */ jsxs4("div", { className: "gs-shortcuts-panel", children: [
1606
2062
  /* @__PURE__ */ jsxs4("div", { className: "gs-shortcuts-header", children: [
1607
2063
  /* @__PURE__ */ jsx4("span", { children: "Keyboard Shortcuts" }),
1608
- /* @__PURE__ */ jsx4("button", { className: "gs-btn-icon", onClick: () => setShowShortcuts(false), style: { pointerEvents: "auto" }, children: "\xD7" })
2064
+ /* @__PURE__ */ jsx4("button", { className: "gs-btn-icon", onClick: () => setShowShortcuts(false), children: "\xD7" })
1609
2065
  ] }),
2066
+ /* @__PURE__ */ jsx4("div", { className: "gs-shortcuts-section-title", children: "Actions" }),
2067
+ /* @__PURE__ */ jsx4("table", { className: "gs-shortcuts-table", children: /* @__PURE__ */ jsx4("tbody", { children: [
2068
+ ["Enter", "Submit", "ENTER"],
2069
+ ["Tab", "Next field", "TAB"],
2070
+ ["Backspace", "Backspace", "BACKSPACE"],
2071
+ ["Delete", "Delete", "DELETE"],
2072
+ ["Insert", "Insert / Overwrite", "INSERT"],
2073
+ ["Home", "Home", "HOME"],
2074
+ ["End", "End", "END"],
2075
+ ["Page Up", "Roll Down", "PAGEUP"],
2076
+ ["Page Down", "Roll Up", "PAGEDOWN"],
2077
+ ["Ctrl+Enter", "Field Exit", "FIELD_EXIT"],
2078
+ ["Ctrl+R", "Reset", "RESET"],
2079
+ ["\u2014", "Help", "HELP"],
2080
+ ["\u2014", "Clear", "CLEAR"],
2081
+ ["\u2014", "Print", "PRINT"]
2082
+ ].map(([label, desc, key]) => /* @__PURE__ */ jsxs4("tr", { className: "gs-shortcut-row", onClick: (e) => {
2083
+ e.stopPropagation();
2084
+ handleShortcutSend(key);
2085
+ }, children: [
2086
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: label }),
2087
+ /* @__PURE__ */ jsx4("td", { children: desc })
2088
+ ] }, key)) }) }),
2089
+ /* @__PURE__ */ jsx4("div", { className: "gs-shortcuts-section-title", children: "Function keys" }),
2090
+ /* @__PURE__ */ jsx4("div", { className: "gs-shortcuts-fkeys", children: Array.from({ length: 24 }, (_, i) => `F${i + 1}`).map((fk) => /* @__PURE__ */ jsx4(
2091
+ "button",
2092
+ {
2093
+ className: "gs-shortcut-fkey",
2094
+ onClick: (e) => {
2095
+ e.stopPropagation();
2096
+ handleShortcutSend(fk);
2097
+ },
2098
+ title: `Send ${fk}`,
2099
+ children: fk
2100
+ },
2101
+ fk
2102
+ )) }),
2103
+ /* @__PURE__ */ jsx4("div", { className: "gs-shortcuts-section-title", children: "Info" }),
1610
2104
  /* @__PURE__ */ jsx4("table", { className: "gs-shortcuts-table", children: /* @__PURE__ */ jsxs4("tbody", { children: [
1611
- /* @__PURE__ */ jsxs4("tr", { children: [
1612
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Ctrl+Enter" }),
1613
- /* @__PURE__ */ jsx4("td", { children: "Field Exit" })
1614
- ] }),
1615
- /* @__PURE__ */ jsxs4("tr", { children: [
1616
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Ctrl+R" }),
1617
- /* @__PURE__ */ jsx4("td", { children: "Reset" })
1618
- ] }),
1619
- /* @__PURE__ */ jsxs4("tr", { children: [
1620
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Insert" }),
1621
- /* @__PURE__ */ jsx4("td", { children: "Insert / Overwrite" })
1622
- ] }),
1623
- /* @__PURE__ */ jsxs4("tr", { children: [
1624
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Page Up" }),
1625
- /* @__PURE__ */ jsx4("td", { children: "Roll Down" })
1626
- ] }),
1627
- /* @__PURE__ */ jsxs4("tr", { children: [
1628
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Page Down" }),
1629
- /* @__PURE__ */ jsx4("td", { children: "Roll Up" })
1630
- ] }),
1631
2105
  /* @__PURE__ */ jsxs4("tr", { children: [
1632
2106
  /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Click" }),
1633
2107
  /* @__PURE__ */ jsx4("td", { children: "Focus / Position cursor" })
@@ -1640,7 +2114,8 @@ function GreenScreenTerminal({
1640
2114
  ] }),
1641
2115
  connStatus && !connStatus.connected && screenData && /* @__PURE__ */ jsxs4("div", { className: "gs-overlay", children: [
1642
2116
  /* @__PURE__ */ jsx4(WifiOffIcon, { size: 28 }),
1643
- /* @__PURE__ */ jsx4("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : "Disconnected" })
2117
+ /* @__PURE__ */ jsx4("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : connStatus?.status === "connecting" ? "Connecting..." : "Disconnected" }),
2118
+ connStatus.error && !isAutoReconnecting && !reconnecting && /* @__PURE__ */ jsx4("span", { style: { fontSize: "0.75em", opacity: 0.7, maxWidth: "80%", textAlign: "center", wordBreak: "break-word" }, children: connStatus.error })
1644
2119
  ] }),
1645
2120
  /* @__PURE__ */ jsx4(
1646
2121
  "input",
@@ -1654,7 +2129,19 @@ function GreenScreenTerminal({
1654
2129
  autoComplete: "off",
1655
2130
  autoCorrect: "off",
1656
2131
  autoCapitalize: "off",
1657
- spellCheck: false
2132
+ spellCheck: false,
2133
+ lang: (() => {
2134
+ const f = getCurrentField();
2135
+ if (!f) return void 0;
2136
+ if (f.is_dbcs || f.is_dbcs_either) return "ja";
2137
+ return void 0;
2138
+ })(),
2139
+ inputMode: (() => {
2140
+ const f = getCurrentField();
2141
+ if (!f) return void 0;
2142
+ if (f.is_dbcs) return "text";
2143
+ return "text";
2144
+ })()
1658
2145
  }
1659
2146
  )
1660
2147
  ]