green-screen-react 1.2.2 → 1.2.3

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
@@ -1,5 +1,5 @@
1
1
  // src/components/GreenScreenTerminal.tsx
2
- import { useState as useState5, useEffect as useEffect4, useRef as useRef4, useCallback as useCallback3, useMemo as useMemo2 } from "react";
2
+ import { useState as useState5, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback3, useMemo as useMemo2 } from "react";
3
3
 
4
4
  // src/adapters/RestAdapter.ts
5
5
  var RestAdapter = class {
@@ -80,6 +80,7 @@ var WebSocketAdapter = class _WebSocketAdapter {
80
80
  this.pendingScreenResolver = null;
81
81
  this.pendingConnectResolver = null;
82
82
  this.pendingMdtResolver = null;
83
+ this.disconnectAckResolver = null;
83
84
  this.connectingPromise = null;
84
85
  this.screenListeners = /* @__PURE__ */ new Set();
85
86
  this.statusListeners = /* @__PURE__ */ new Set();
@@ -214,7 +215,19 @@ var WebSocketAdapter = class _WebSocketAdapter {
214
215
  });
215
216
  }
216
217
  async disconnect() {
218
+ const acked = new Promise((resolve) => {
219
+ const timer = setTimeout(resolve, 3e3);
220
+ this.disconnectAckResolver = () => {
221
+ clearTimeout(timer);
222
+ resolve();
223
+ };
224
+ });
217
225
  this.wsSend({ type: "disconnect" });
226
+ try {
227
+ await acked;
228
+ } finally {
229
+ this.disconnectAckResolver = null;
230
+ }
218
231
  this.status = { connected: false, status: "disconnected" };
219
232
  this._sessionId = null;
220
233
  if (this.ws) {
@@ -270,6 +283,14 @@ var WebSocketAdapter = class _WebSocketAdapter {
270
283
  break;
271
284
  }
272
285
  case "cursor": {
286
+ if (this.screen) {
287
+ this.screen = {
288
+ ...this.screen,
289
+ cursor_row: msg.data.cursor_row,
290
+ cursor_col: msg.data.cursor_col
291
+ };
292
+ for (const listener of this.screenListeners) listener(this.screen);
293
+ }
273
294
  if (this.pendingScreenResolver) {
274
295
  const resolver = this.pendingScreenResolver;
275
296
  this.pendingScreenResolver = null;
@@ -310,6 +331,13 @@ var WebSocketAdapter = class _WebSocketAdapter {
310
331
  resolver({ success: true });
311
332
  }
312
333
  break;
334
+ case "disconnected":
335
+ if (this.disconnectAckResolver) {
336
+ const resolver = this.disconnectAckResolver;
337
+ this.disconnectAckResolver = null;
338
+ resolver();
339
+ }
340
+ break;
313
341
  case "error": {
314
342
  if (this.pendingConnectResolver) {
315
343
  const resolver = this.pendingConnectResolver;
@@ -981,8 +1009,27 @@ var KeyboardIcon = ({ size = 14 }) => /* @__PURE__ */ jsxs2("svg", { width: size
981
1009
  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" }) });
982
1010
 
983
1011
  // src/components/InlineSignIn.tsx
984
- import { useState as useState4 } from "react";
1012
+ import { useEffect as useEffect4, useState as useState4 } from "react";
985
1013
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1014
+ var STORAGE_KEY = "green-screen:inline-signin";
1015
+ function loadStored() {
1016
+ if (typeof window === "undefined") return {};
1017
+ try {
1018
+ const raw = window.localStorage.getItem(STORAGE_KEY);
1019
+ if (!raw) return {};
1020
+ const parsed = JSON.parse(raw);
1021
+ return parsed && typeof parsed === "object" ? parsed : {};
1022
+ } catch {
1023
+ return {};
1024
+ }
1025
+ }
1026
+ function saveStored(value) {
1027
+ if (typeof window === "undefined") return;
1028
+ try {
1029
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
1030
+ } catch {
1031
+ }
1032
+ }
986
1033
  var PROTOCOL_OPTIONS = [
987
1034
  { value: "tn5250", label: "TN5250 (IBM i)" },
988
1035
  { value: "tn3270", label: "TN3270 (Mainframe)" },
@@ -1002,12 +1049,22 @@ var TERMINAL_TYPE_OPTIONS = {
1002
1049
  ]
1003
1050
  };
1004
1051
  function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConnect }) {
1005
- const [host, setHost] = useState4("");
1006
- const [port, setPort] = useState4("");
1007
- const [selectedProtocol, setSelectedProtocol] = useState4(defaultProtocol);
1008
- const [terminalType, setTerminalType] = useState4("");
1009
- const [username, setUsername] = useState4("");
1052
+ const stored = loadStored();
1053
+ const [host, setHost] = useState4(stored.host ?? "");
1054
+ const [port, setPort] = useState4(stored.port ?? "");
1055
+ const [selectedProtocol, setSelectedProtocol] = useState4(stored.protocol ?? defaultProtocol);
1056
+ const [terminalType, setTerminalType] = useState4(stored.terminalType ?? "");
1057
+ const [username, setUsername] = useState4(stored.username ?? "");
1010
1058
  const [password, setPassword] = useState4("");
1059
+ useEffect4(() => {
1060
+ saveStored({
1061
+ host,
1062
+ port,
1063
+ protocol: selectedProtocol,
1064
+ terminalType,
1065
+ username
1066
+ });
1067
+ }, [host, port, selectedProtocol, terminalType, username]);
1011
1068
  const termTypeOptions = TERMINAL_TYPE_OPTIONS[selectedProtocol];
1012
1069
  const [submitted, setSubmitted] = useState4(false);
1013
1070
  const loading = externalLoading || submitted;
@@ -1285,6 +1342,22 @@ function validateMod11(value) {
1285
1342
 
1286
1343
  // src/components/GreenScreenTerminal.tsx
1287
1344
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1345
+ function isBlankContent(content) {
1346
+ if (!content) return true;
1347
+ for (let i = 0; i < content.length; i++) {
1348
+ const ch = content.charCodeAt(i);
1349
+ if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13 && ch !== 0 && ch !== 160) {
1350
+ return false;
1351
+ }
1352
+ }
1353
+ return true;
1354
+ }
1355
+ function formatBusyClock(ms) {
1356
+ const sec = Math.floor(ms / 1e3);
1357
+ const m = Math.floor(sec / 60);
1358
+ const s = sec % 60;
1359
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
1360
+ }
1288
1361
  function aidByteToKeyName(aid) {
1289
1362
  if (aid === 241) return "ENTER";
1290
1363
  if (aid === 243) return "HELP";
@@ -1359,7 +1432,33 @@ function GreenScreenTerminal({
1359
1432
  const { data: polledScreenData, error: screenError } = useTerminalScreen(adapter, pollInterval, shouldPoll);
1360
1433
  const { sendText: _sendText, sendKey: _sendKey } = useTerminalInput(adapter);
1361
1434
  const { connect, reconnect, loading: reconnecting, error: connectError } = useTerminalConnection(adapter);
1362
- const rawScreenData = externalScreenData ?? polledScreenData;
1435
+ const incomingScreenData = externalScreenData ?? polledScreenData;
1436
+ const lastContentfulRef = useRef4(null);
1437
+ useEffect5(() => {
1438
+ if (incomingScreenData && !isBlankContent(incomingScreenData.content)) {
1439
+ lastContentfulRef.current = incomingScreenData;
1440
+ }
1441
+ }, [incomingScreenData]);
1442
+ const isBlankLocked = !!(incomingScreenData && incomingScreenData.keyboard_locked && isBlankContent(incomingScreenData.content));
1443
+ const rawScreenData = useMemo2(() => {
1444
+ if (isBlankLocked && lastContentfulRef.current) {
1445
+ return { ...lastContentfulRef.current, keyboard_locked: true };
1446
+ }
1447
+ return incomingScreenData;
1448
+ }, [incomingScreenData, isBlankLocked]);
1449
+ const BUSY_OVERLAY_DELAY_MS = 600;
1450
+ const [lockElapsedMs, setLockElapsedMs] = useState5(0);
1451
+ useEffect5(() => {
1452
+ if (!isBlankLocked) {
1453
+ setLockElapsedMs(0);
1454
+ return;
1455
+ }
1456
+ const start = Date.now();
1457
+ setLockElapsedMs(0);
1458
+ const id = setInterval(() => setLockElapsedMs(Date.now() - start), 250);
1459
+ return () => clearInterval(id);
1460
+ }, [isBlankLocked]);
1461
+ const showBusyOverlay = isBlankLocked && lockElapsedMs >= BUSY_OVERLAY_DELAY_MS;
1363
1462
  const connStatus = externalStatus ?? (rawScreenData ? { connected: true, status: "authenticated" } : { connected: false, status: "disconnected" });
1364
1463
  const { displayedContent, animatedCursorPos } = useTypingAnimation(
1365
1464
  rawScreenData?.content,
@@ -1371,7 +1470,7 @@ function GreenScreenTerminal({
1371
1470
  return { ...rawScreenData, content: displayedContent };
1372
1471
  }, [rawScreenData, displayedContent]);
1373
1472
  const prevScreenSigRef = useRef4(void 0);
1374
- useEffect4(() => {
1473
+ useEffect5(() => {
1375
1474
  if (screenData && onScreenChange && screenData.screen_signature !== prevScreenSigRef.current) {
1376
1475
  prevScreenSigRef.current = screenData.screen_signature;
1377
1476
  onScreenChange(screenData);
@@ -1381,7 +1480,7 @@ function GreenScreenTerminal({
1381
1480
  const sendKey = useCallback3(async (key) => _sendKey(key), [_sendKey]);
1382
1481
  const [optimisticEdits, setOptimisticEdits] = useState5([]);
1383
1482
  const prevScreenContentForEdits = useRef4(void 0);
1384
- useEffect4(() => {
1483
+ useEffect5(() => {
1385
1484
  const content = rawScreenData?.content;
1386
1485
  if (content && content !== prevScreenContentForEdits.current) {
1387
1486
  prevScreenContentForEdits.current = content;
@@ -1395,7 +1494,7 @@ function GreenScreenTerminal({
1395
1494
  const inputRef = useRef4(null);
1396
1495
  const [syncedCursor, setSyncedCursor] = useState5(null);
1397
1496
  const prevRawContentRef = useRef4("");
1398
- useEffect4(() => {
1497
+ useEffect5(() => {
1399
1498
  const newContent = rawScreenData?.content || "";
1400
1499
  if (prevRawContentRef.current && newContent && newContent !== prevRawContentRef.current) {
1401
1500
  setSyncedCursor(null);
@@ -1408,10 +1507,10 @@ function GreenScreenTerminal({
1408
1507
  const reconnectTimeoutRef = useRef4(null);
1409
1508
  const wasConnectedRef = useRef4(false);
1410
1509
  const isConnectedRef = useRef4(false);
1411
- useEffect4(() => {
1510
+ useEffect5(() => {
1412
1511
  isConnectedRef.current = connStatus?.connected ?? false;
1413
1512
  }, [connStatus?.connected]);
1414
- useEffect4(() => {
1513
+ useEffect5(() => {
1415
1514
  if (!autoReconnectEnabled) return;
1416
1515
  const isConnected = connStatus?.connected;
1417
1516
  if (isConnected) {
@@ -1470,12 +1569,12 @@ function GreenScreenTerminal({
1470
1569
  setConnecting(false);
1471
1570
  }
1472
1571
  }, [connect, onSignIn, externalAdapter, baseUrlAdapter]);
1473
- useEffect4(() => {
1572
+ useEffect5(() => {
1474
1573
  if (connecting && screenData?.content) setConnecting(false);
1475
1574
  }, [connecting, screenData?.content]);
1476
1575
  const [showBootLoader, setShowBootLoader] = useState5(bootLoader !== false);
1477
1576
  const [bootFadingOut, setBootFadingOut] = useState5(false);
1478
- useEffect4(() => {
1577
+ useEffect5(() => {
1479
1578
  if (!showBootLoader) return;
1480
1579
  const shouldDismiss = bootLoaderReady !== void 0 ? bootLoaderReady : !!screenData?.content;
1481
1580
  if (shouldDismiss) {
@@ -1486,7 +1585,7 @@ function GreenScreenTerminal({
1486
1585
  }
1487
1586
  }, [screenData?.content, showBootLoader, bootLoaderReady]);
1488
1587
  const FOCUS_STORAGE_KEY = "gs-terminal-focused";
1489
- useEffect4(() => {
1588
+ useEffect5(() => {
1490
1589
  if (!autoFocusDisabled && !readOnly) {
1491
1590
  try {
1492
1591
  if (localStorage.getItem(FOCUS_STORAGE_KEY) === "true") {
@@ -1496,32 +1595,32 @@ function GreenScreenTerminal({
1496
1595
  }
1497
1596
  }
1498
1597
  }, []);
1499
- useEffect4(() => {
1598
+ useEffect5(() => {
1500
1599
  if (autoFocusDisabled) return;
1501
1600
  try {
1502
1601
  localStorage.setItem(FOCUS_STORAGE_KEY, String(isFocused));
1503
1602
  } catch {
1504
1603
  }
1505
1604
  }, [isFocused, autoFocusDisabled]);
1506
- useEffect4(() => {
1605
+ useEffect5(() => {
1507
1606
  if (isFocused) inputRef.current?.focus();
1508
1607
  }, [isFocused]);
1509
1608
  const hadScreenData = useRef4(false);
1510
- useEffect4(() => {
1609
+ useEffect5(() => {
1511
1610
  if (screenData?.content && !hadScreenData.current && !autoFocusDisabled && !readOnly) {
1512
1611
  hadScreenData.current = true;
1513
1612
  setIsFocused(true);
1514
1613
  }
1515
1614
  if (!screenData?.content) hadScreenData.current = false;
1516
1615
  }, [screenData?.content, autoFocusDisabled, readOnly]);
1517
- useEffect4(() => {
1616
+ useEffect5(() => {
1518
1617
  const handleClickOutside = (event) => {
1519
1618
  if (terminalRef.current && !terminalRef.current.contains(event.target)) setIsFocused(false);
1520
1619
  };
1521
1620
  if (isFocused) document.addEventListener("mousedown", handleClickOutside);
1522
1621
  return () => document.removeEventListener("mousedown", handleClickOutside);
1523
1622
  }, [isFocused]);
1524
- useEffect4(() => {
1623
+ useEffect5(() => {
1525
1624
  if (readOnly && isFocused) {
1526
1625
  setIsFocused(false);
1527
1626
  inputRef.current?.blur();
@@ -1907,7 +2006,7 @@ function GreenScreenTerminal({
1907
2006
  const cursor = getCursorPos();
1908
2007
  const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
1909
2008
  const cursorInInputField = hasCursor && fields.some(
1910
- (f) => f.is_input && !f.is_non_display && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
2009
+ (f) => f.is_input && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
1911
2010
  );
1912
2011
  return /* @__PURE__ */ jsxs4("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
1913
2012
  rows.map((line, index) => {
@@ -2117,6 +2216,14 @@ function GreenScreenTerminal({
2117
2216
  /* @__PURE__ */ jsx4("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : connStatus?.status === "connecting" ? "Connecting..." : "Disconnected" }),
2118
2217
  connStatus.error && !isAutoReconnecting && !reconnecting && /* @__PURE__ */ jsx4("span", { style: { fontSize: "0.75em", opacity: 0.7, maxWidth: "80%", textAlign: "center", wordBreak: "break-word" }, children: connStatus.error })
2119
2218
  ] }),
2219
+ showBusyOverlay && connStatus?.connected && /* @__PURE__ */ jsxs4("div", { className: "gs-busy-overlay", role: "status", "aria-live": "polite", children: [
2220
+ /* @__PURE__ */ jsx4(RefreshIcon, { size: 22, className: "gs-spin" }),
2221
+ /* @__PURE__ */ jsxs4("span", { className: "gs-busy-clock", children: [
2222
+ "X CLOCK\xA0\xA0",
2223
+ formatBusyClock(lockElapsedMs)
2224
+ ] }),
2225
+ /* @__PURE__ */ jsx4("span", { className: "gs-busy-hint", children: "Waiting for host\u2026" })
2226
+ ] }),
2120
2227
  /* @__PURE__ */ jsx4(
2121
2228
  "input",
2122
2229
  {