green-screen-react 1.1.3 → 1.2.1

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,7 +1326,6 @@ function GreenScreenTerminal({
1066
1326
  inlineSignIn = true,
1067
1327
  defaultProtocol: signInDefaultProtocol,
1068
1328
  onSignIn,
1069
- autoSignedIn,
1070
1329
  autoFocusDisabled = false,
1071
1330
  bootLoader,
1072
1331
  bootLoaderReady,
@@ -1076,6 +1335,7 @@ function GreenScreenTerminal({
1076
1335
  onNotification,
1077
1336
  onScreenChange,
1078
1337
  onMinimize,
1338
+ showShortcutsButton = true,
1079
1339
  className,
1080
1340
  style
1081
1341
  }) {
@@ -1130,13 +1390,7 @@ function GreenScreenTerminal({
1130
1390
  }, [rawScreenData?.content]);
1131
1391
  const [inputText, setInputText] = useState5("");
1132
1392
  const [isFocused, setIsFocused] = useState5(false);
1133
- const [showSignInHint, setShowSignInHint] = useState5(false);
1134
1393
  const [showShortcuts, setShowShortcuts] = useState5(false);
1135
- const prevAutoSignedIn = useRef4(false);
1136
- useEffect4(() => {
1137
- if (autoSignedIn && !prevAutoSignedIn.current) setShowSignInHint(true);
1138
- prevAutoSignedIn.current = !!autoSignedIn;
1139
- }, [autoSignedIn]);
1140
1394
  const terminalRef = useRef4(null);
1141
1395
  const inputRef = useRef4(null);
1142
1396
  const [syncedCursor, setSyncedCursor] = useState5(null);
@@ -1146,7 +1400,6 @@ function GreenScreenTerminal({
1146
1400
  if (prevRawContentRef.current && newContent && newContent !== prevRawContentRef.current) {
1147
1401
  setSyncedCursor(null);
1148
1402
  setInputText("");
1149
- if (showSignInHint) setShowSignInHint(false);
1150
1403
  }
1151
1404
  prevRawContentRef.current = newContent;
1152
1405
  }, [rawScreenData?.content]);
@@ -1209,11 +1462,9 @@ function GreenScreenTerminal({
1209
1462
  const newAdapter = new RestAdapter({ baseUrl: `http://${config.host}${port}` });
1210
1463
  setInternalAdapter(newAdapter);
1211
1464
  await newAdapter.connect(config);
1212
- if (config.username && config.password) setShowSignInHint(true);
1213
1465
  return;
1214
1466
  }
1215
1467
  await connect(config);
1216
- if (config.username && config.password) setShowSignInHint(true);
1217
1468
  } catch (err) {
1218
1469
  setSignInError(err instanceof Error ? err.message : String(err));
1219
1470
  setConnecting(false);
@@ -1301,13 +1552,25 @@ function GreenScreenTerminal({
1301
1552
  const clickedRow = Math.floor(y / ROW_HEIGHT);
1302
1553
  const clickedCol = Math.floor(x / charWidth);
1303
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
+ }
1304
1567
  setSyncedCursor({ row: clickedRow, col: clickedCol });
1305
1568
  adapter.setCursor?.(clickedRow, clickedCol).then((r) => {
1306
1569
  if (r?.cursor_row !== void 0) {
1307
1570
  setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1308
1571
  }
1309
1572
  });
1310
- }, [readOnly, screenData, adapter]);
1573
+ }, [readOnly, screenData, adapter, sendKey]);
1311
1574
  const getCurrentField = useCallback3(() => {
1312
1575
  const fields = screenData?.fields || [];
1313
1576
  const cursorRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
@@ -1317,6 +1580,31 @@ function GreenScreenTerminal({
1317
1580
  }
1318
1581
  return null;
1319
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]);
1320
1608
  const handleKeyDown = async (e) => {
1321
1609
  if (readOnly) {
1322
1610
  e.preventDefault();
@@ -1355,18 +1643,19 @@ function GreenScreenTerminal({
1355
1643
  End: "END",
1356
1644
  Insert: "INSERT"
1357
1645
  };
1358
- if (e.key.startsWith("F") && e.key.length <= 3) {
1646
+ if (/^F([1-9]|1[0-9]|2[0-4])$/.test(e.key)) {
1359
1647
  e.preventDefault();
1360
- const fKey = e.key.toUpperCase();
1361
- if (/^F([1-9]|1[0-9]|2[0-4])$/.test(fKey)) {
1362
- const kr = await sendKey(fKey);
1363
- if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1364
- return;
1365
- }
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;
1366
1652
  }
1367
1653
  if (keyMap[e.key]) {
1368
1654
  e.preventDefault();
1369
- 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);
1370
1659
  if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1371
1660
  }
1372
1661
  };
@@ -1377,18 +1666,32 @@ function GreenScreenTerminal({
1377
1666
  }
1378
1667
  const newText = e.target.value;
1379
1668
  if (newText.length > inputText.length) {
1380
- const newChars = newText.substring(inputText.length);
1381
- const curRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
1382
- const curCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1383
- const edits = [];
1384
- for (let i = 0; i < newChars.length; i++) {
1385
- 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
+ });
1386
1694
  }
1387
- setOptimisticEdits((prev) => [...prev, ...edits]);
1388
- setSyncedCursor({ row: curRow, col: curCol + newChars.length });
1389
- sendText(newChars).then((r) => {
1390
- if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1391
- });
1392
1695
  }
1393
1696
  setInputText("");
1394
1697
  e.target.value = "";
@@ -1420,41 +1723,156 @@ function GreenScreenTerminal({
1420
1723
  if (lastIndex < text.length) segments.push(/* @__PURE__ */ jsx4("span", { children: text.substring(lastIndex) }, `${keyPrefix}-e`));
1421
1724
  return segments.length > 0 ? /* @__PURE__ */ jsx4(Fragment, { children: segments }) : /* @__PURE__ */ jsx4(Fragment, { children: text });
1422
1725
  }, []);
1423
- const renderRowWithFields = useCallback3((line, rowIndex, fields) => {
1726
+ const renderRowWithFields = useCallback3((line, rowIndex, fields, cursorRow, cursorCol) => {
1424
1727
  const inputFields = fields.filter((f) => f.row === rowIndex && f.is_input);
1425
1728
  const highlightedFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_highlighted);
1426
1729
  const reverseFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_reverse);
1427
1730
  const colorFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.color && !f.is_highlighted && !f.is_reverse);
1428
1731
  const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields, ...colorFields];
1429
- 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 });
1430
1749
  const sorted = [...allRowFields].sort((a, b) => a.col - b.col);
1431
1750
  const segs = [];
1432
1751
  let lastEnd = 0;
1433
- 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
+ };
1434
1811
  sorted.forEach((field, idx) => {
1435
1812
  const fs = field.col;
1436
1813
  const fe = Math.min(field.col + field.length, cols);
1437
- if (fs > lastEnd) segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(lastEnd, fs) }, `t${idx}`));
1814
+ if (fs > lastEnd) renderPlainRun(lastEnd, fs, `t${idx}`);
1438
1815
  const fc = line.substring(fs, fe);
1439
- 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;
1440
1830
  if (field.is_input) {
1441
1831
  const fieldWidth = Math.min(field.length, cols - fs);
1442
- const fieldClass = showSignInHint ? "gs-confirmed-field" : field.is_underscored ? "gs-input-field" : void 0;
1443
- 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
+ );
1444
1853
  } else if (field.is_reverse) {
1445
- segs.push(/* @__PURE__ */ jsx4("span", { style: { color: colorVar || "var(--gs-red, #FF5555)", fontWeight: "bold" }, children: fc }, `v${idx}`));
1446
- } else if (colorVar) {
1447
- 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}`));
1448
1866
  } else if (field.is_highlighted) {
1449
- 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}`));
1450
1868
  } else {
1451
- segs.push(/* @__PURE__ */ jsx4("span", { children: fc }, `h${idx}`));
1869
+ segs.push(/* @__PURE__ */ jsx4("span", { children: displayText }, `h${idx}`));
1452
1870
  }
1453
1871
  lastEnd = fe;
1454
1872
  });
1455
- if (lastEnd < line.length) segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(lastEnd) }, "te"));
1873
+ if (lastEnd < line.length) renderPlainRun(lastEnd, line.length, "te");
1456
1874
  return /* @__PURE__ */ jsx4(Fragment, { children: segs });
1457
- }, [renderTextWithUnderlines, screenData?.cols, profile.defaultCols, showSignInHint]);
1875
+ }, [renderTextWithUnderlines, screenData, profile.defaultCols]);
1458
1876
  const renderScreen = () => {
1459
1877
  if (showBootLoader && !screenData?.content) {
1460
1878
  if (bootLoader === false) return null;
@@ -1471,7 +1889,7 @@ function GreenScreenTerminal({
1471
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" }),
1472
1890
  !connStatus?.connected && isUsingDefaultAdapter && /* @__PURE__ */ jsxs4("p", { style: { fontFamily: "var(--gs-font)", fontSize: "11px", color: "#606060", marginTop: "8px" }, children: [
1473
1891
  "Start the proxy: ",
1474
- /* @__PURE__ */ jsx4("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy --mock" })
1892
+ /* @__PURE__ */ jsx4("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy" })
1475
1893
  ] })
1476
1894
  ] }) });
1477
1895
  }
@@ -1504,11 +1922,33 @@ function GreenScreenTerminal({
1504
1922
  }
1505
1923
  const headerSegments = index === 0 ? profile.colors.parseHeaderRow(displayLine) : null;
1506
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: [
1507
- headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ jsx4("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields),
1508
- 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" } })
1509
1927
  ] }, index);
1510
1928
  }),
1511
- 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
+ ),
1512
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: [
1513
1953
  String(screenData.cursor_row + 1).padStart(2, "0"),
1514
1954
  "/",
@@ -1539,6 +1979,15 @@ function GreenScreenTerminal({
1539
1979
  return "#64748b";
1540
1980
  }
1541
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]);
1542
1991
  return /* @__PURE__ */ jsxs4("div", { className: `gs-terminal ${isFocused ? "gs-terminal-focused" : ""} ${className || ""}`, style, children: [
1543
1992
  showHeader && /* @__PURE__ */ jsx4("div", { className: "gs-header", children: embedded ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1544
1993
  /* @__PURE__ */ jsxs4("span", { className: "gs-header-left", children: [
@@ -1558,10 +2007,10 @@ function GreenScreenTerminal({
1558
2007
  e.stopPropagation();
1559
2008
  onMinimize();
1560
2009
  }, className: "gs-btn-icon", title: "Minimize terminal", children: /* @__PURE__ */ jsx4(MinimizeIcon, {}) }),
1561
- /* @__PURE__ */ jsx4("button", { onClick: (e) => {
2010
+ showShortcutsButton && /* @__PURE__ */ jsx4("button", { onClick: (e) => {
1562
2011
  e.stopPropagation();
1563
2012
  setShowShortcuts((s) => !s);
1564
- }, 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 }) }),
1565
2014
  headerRight
1566
2015
  ] })
1567
2016
  ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
@@ -1588,10 +2037,10 @@ function GreenScreenTerminal({
1588
2037
  connStatus.username && /* @__PURE__ */ jsx4("span", { className: "gs-host", children: connStatus.username })
1589
2038
  ] }),
1590
2039
  statusActions,
1591
- /* @__PURE__ */ jsx4("button", { onClick: (e) => {
2040
+ showShortcutsButton && /* @__PURE__ */ jsx4("button", { onClick: (e) => {
1592
2041
  e.stopPropagation();
1593
2042
  setShowShortcuts((s) => !s);
1594
- }, 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 }) }),
1595
2044
  headerRight
1596
2045
  ] })
1597
2046
  ] }) }),
@@ -1612,29 +2061,47 @@ function GreenScreenTerminal({
1612
2061
  showShortcuts && /* @__PURE__ */ jsxs4("div", { className: "gs-shortcuts-panel", children: [
1613
2062
  /* @__PURE__ */ jsxs4("div", { className: "gs-shortcuts-header", children: [
1614
2063
  /* @__PURE__ */ jsx4("span", { children: "Keyboard Shortcuts" }),
1615
- /* @__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" })
1616
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" }),
1617
2104
  /* @__PURE__ */ jsx4("table", { className: "gs-shortcuts-table", children: /* @__PURE__ */ jsxs4("tbody", { children: [
1618
- /* @__PURE__ */ jsxs4("tr", { children: [
1619
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Ctrl+Enter" }),
1620
- /* @__PURE__ */ jsx4("td", { children: "Field Exit" })
1621
- ] }),
1622
- /* @__PURE__ */ jsxs4("tr", { children: [
1623
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Ctrl+R" }),
1624
- /* @__PURE__ */ jsx4("td", { children: "Reset" })
1625
- ] }),
1626
- /* @__PURE__ */ jsxs4("tr", { children: [
1627
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Insert" }),
1628
- /* @__PURE__ */ jsx4("td", { children: "Insert / Overwrite" })
1629
- ] }),
1630
- /* @__PURE__ */ jsxs4("tr", { children: [
1631
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Page Up" }),
1632
- /* @__PURE__ */ jsx4("td", { children: "Roll Down" })
1633
- ] }),
1634
- /* @__PURE__ */ jsxs4("tr", { children: [
1635
- /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Page Down" }),
1636
- /* @__PURE__ */ jsx4("td", { children: "Roll Up" })
1637
- ] }),
1638
2105
  /* @__PURE__ */ jsxs4("tr", { children: [
1639
2106
  /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Click" }),
1640
2107
  /* @__PURE__ */ jsx4("td", { children: "Focus / Position cursor" })
@@ -1662,7 +2129,19 @@ function GreenScreenTerminal({
1662
2129
  autoComplete: "off",
1663
2130
  autoCorrect: "off",
1664
2131
  autoCapitalize: "off",
1665
- 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
+ })()
1666
2145
  }
1667
2146
  )
1668
2147
  ]