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.d.mts CHANGED
@@ -277,6 +277,7 @@ declare class WebSocketAdapter implements TerminalAdapter {
277
277
  private pendingScreenResolver;
278
278
  private pendingConnectResolver;
279
279
  private pendingMdtResolver;
280
+ private disconnectAckResolver;
280
281
  private connectingPromise;
281
282
  private screenListeners;
282
283
  private statusListeners;
package/dist/index.d.ts CHANGED
@@ -277,6 +277,7 @@ declare class WebSocketAdapter implements TerminalAdapter {
277
277
  private pendingScreenResolver;
278
278
  private pendingConnectResolver;
279
279
  private pendingMdtResolver;
280
+ private disconnectAckResolver;
280
281
  private connectingPromise;
281
282
  private screenListeners;
282
283
  private statusListeners;
package/dist/index.js CHANGED
@@ -131,6 +131,7 @@ var WebSocketAdapter = class _WebSocketAdapter {
131
131
  this.pendingScreenResolver = null;
132
132
  this.pendingConnectResolver = null;
133
133
  this.pendingMdtResolver = null;
134
+ this.disconnectAckResolver = null;
134
135
  this.connectingPromise = null;
135
136
  this.screenListeners = /* @__PURE__ */ new Set();
136
137
  this.statusListeners = /* @__PURE__ */ new Set();
@@ -265,7 +266,19 @@ var WebSocketAdapter = class _WebSocketAdapter {
265
266
  });
266
267
  }
267
268
  async disconnect() {
269
+ const acked = new Promise((resolve) => {
270
+ const timer = setTimeout(resolve, 3e3);
271
+ this.disconnectAckResolver = () => {
272
+ clearTimeout(timer);
273
+ resolve();
274
+ };
275
+ });
268
276
  this.wsSend({ type: "disconnect" });
277
+ try {
278
+ await acked;
279
+ } finally {
280
+ this.disconnectAckResolver = null;
281
+ }
269
282
  this.status = { connected: false, status: "disconnected" };
270
283
  this._sessionId = null;
271
284
  if (this.ws) {
@@ -321,6 +334,14 @@ var WebSocketAdapter = class _WebSocketAdapter {
321
334
  break;
322
335
  }
323
336
  case "cursor": {
337
+ if (this.screen) {
338
+ this.screen = {
339
+ ...this.screen,
340
+ cursor_row: msg.data.cursor_row,
341
+ cursor_col: msg.data.cursor_col
342
+ };
343
+ for (const listener of this.screenListeners) listener(this.screen);
344
+ }
324
345
  if (this.pendingScreenResolver) {
325
346
  const resolver = this.pendingScreenResolver;
326
347
  this.pendingScreenResolver = null;
@@ -361,6 +382,13 @@ var WebSocketAdapter = class _WebSocketAdapter {
361
382
  resolver({ success: true });
362
383
  }
363
384
  break;
385
+ case "disconnected":
386
+ if (this.disconnectAckResolver) {
387
+ const resolver = this.disconnectAckResolver;
388
+ this.disconnectAckResolver = null;
389
+ resolver();
390
+ }
391
+ break;
364
392
  case "error": {
365
393
  if (this.pendingConnectResolver) {
366
394
  const resolver = this.pendingConnectResolver;
@@ -1034,6 +1062,25 @@ var MinimizeIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { w
1034
1062
  // src/components/InlineSignIn.tsx
1035
1063
  var import_react4 = require("react");
1036
1064
  var import_jsx_runtime3 = require("react/jsx-runtime");
1065
+ var STORAGE_KEY = "green-screen:inline-signin";
1066
+ function loadStored() {
1067
+ if (typeof window === "undefined") return {};
1068
+ try {
1069
+ const raw = window.localStorage.getItem(STORAGE_KEY);
1070
+ if (!raw) return {};
1071
+ const parsed = JSON.parse(raw);
1072
+ return parsed && typeof parsed === "object" ? parsed : {};
1073
+ } catch {
1074
+ return {};
1075
+ }
1076
+ }
1077
+ function saveStored(value) {
1078
+ if (typeof window === "undefined") return;
1079
+ try {
1080
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
1081
+ } catch {
1082
+ }
1083
+ }
1037
1084
  var PROTOCOL_OPTIONS = [
1038
1085
  { value: "tn5250", label: "TN5250 (IBM i)" },
1039
1086
  { value: "tn3270", label: "TN3270 (Mainframe)" },
@@ -1053,12 +1100,22 @@ var TERMINAL_TYPE_OPTIONS = {
1053
1100
  ]
1054
1101
  };
1055
1102
  function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConnect }) {
1056
- const [host, setHost] = (0, import_react4.useState)("");
1057
- const [port, setPort] = (0, import_react4.useState)("");
1058
- const [selectedProtocol, setSelectedProtocol] = (0, import_react4.useState)(defaultProtocol);
1059
- const [terminalType, setTerminalType] = (0, import_react4.useState)("");
1060
- const [username, setUsername] = (0, import_react4.useState)("");
1103
+ const stored = loadStored();
1104
+ const [host, setHost] = (0, import_react4.useState)(stored.host ?? "");
1105
+ const [port, setPort] = (0, import_react4.useState)(stored.port ?? "");
1106
+ const [selectedProtocol, setSelectedProtocol] = (0, import_react4.useState)(stored.protocol ?? defaultProtocol);
1107
+ const [terminalType, setTerminalType] = (0, import_react4.useState)(stored.terminalType ?? "");
1108
+ const [username, setUsername] = (0, import_react4.useState)(stored.username ?? "");
1061
1109
  const [password, setPassword] = (0, import_react4.useState)("");
1110
+ (0, import_react4.useEffect)(() => {
1111
+ saveStored({
1112
+ host,
1113
+ port,
1114
+ protocol: selectedProtocol,
1115
+ terminalType,
1116
+ username
1117
+ });
1118
+ }, [host, port, selectedProtocol, terminalType, username]);
1062
1119
  const termTypeOptions = TERMINAL_TYPE_OPTIONS[selectedProtocol];
1063
1120
  const [submitted, setSubmitted] = (0, import_react4.useState)(false);
1064
1121
  const loading = externalLoading || submitted;
@@ -1336,6 +1393,22 @@ function validateMod11(value) {
1336
1393
 
1337
1394
  // src/components/GreenScreenTerminal.tsx
1338
1395
  var import_jsx_runtime4 = require("react/jsx-runtime");
1396
+ function isBlankContent(content) {
1397
+ if (!content) return true;
1398
+ for (let i = 0; i < content.length; i++) {
1399
+ const ch = content.charCodeAt(i);
1400
+ if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13 && ch !== 0 && ch !== 160) {
1401
+ return false;
1402
+ }
1403
+ }
1404
+ return true;
1405
+ }
1406
+ function formatBusyClock(ms) {
1407
+ const sec = Math.floor(ms / 1e3);
1408
+ const m = Math.floor(sec / 60);
1409
+ const s = sec % 60;
1410
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
1411
+ }
1339
1412
  function aidByteToKeyName(aid) {
1340
1413
  if (aid === 241) return "ENTER";
1341
1414
  if (aid === 243) return "HELP";
@@ -1410,7 +1483,33 @@ function GreenScreenTerminal({
1410
1483
  const { data: polledScreenData, error: screenError } = useTerminalScreen(adapter, pollInterval, shouldPoll);
1411
1484
  const { sendText: _sendText, sendKey: _sendKey } = useTerminalInput(adapter);
1412
1485
  const { connect, reconnect, loading: reconnecting, error: connectError } = useTerminalConnection(adapter);
1413
- const rawScreenData = externalScreenData ?? polledScreenData;
1486
+ const incomingScreenData = externalScreenData ?? polledScreenData;
1487
+ const lastContentfulRef = (0, import_react5.useRef)(null);
1488
+ (0, import_react5.useEffect)(() => {
1489
+ if (incomingScreenData && !isBlankContent(incomingScreenData.content)) {
1490
+ lastContentfulRef.current = incomingScreenData;
1491
+ }
1492
+ }, [incomingScreenData]);
1493
+ const isBlankLocked = !!(incomingScreenData && incomingScreenData.keyboard_locked && isBlankContent(incomingScreenData.content));
1494
+ const rawScreenData = (0, import_react5.useMemo)(() => {
1495
+ if (isBlankLocked && lastContentfulRef.current) {
1496
+ return { ...lastContentfulRef.current, keyboard_locked: true };
1497
+ }
1498
+ return incomingScreenData;
1499
+ }, [incomingScreenData, isBlankLocked]);
1500
+ const BUSY_OVERLAY_DELAY_MS = 600;
1501
+ const [lockElapsedMs, setLockElapsedMs] = (0, import_react5.useState)(0);
1502
+ (0, import_react5.useEffect)(() => {
1503
+ if (!isBlankLocked) {
1504
+ setLockElapsedMs(0);
1505
+ return;
1506
+ }
1507
+ const start = Date.now();
1508
+ setLockElapsedMs(0);
1509
+ const id = setInterval(() => setLockElapsedMs(Date.now() - start), 250);
1510
+ return () => clearInterval(id);
1511
+ }, [isBlankLocked]);
1512
+ const showBusyOverlay = isBlankLocked && lockElapsedMs >= BUSY_OVERLAY_DELAY_MS;
1414
1513
  const connStatus = externalStatus ?? (rawScreenData ? { connected: true, status: "authenticated" } : { connected: false, status: "disconnected" });
1415
1514
  const { displayedContent, animatedCursorPos } = useTypingAnimation(
1416
1515
  rawScreenData?.content,
@@ -1958,7 +2057,7 @@ function GreenScreenTerminal({
1958
2057
  const cursor = getCursorPos();
1959
2058
  const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
1960
2059
  const cursorInInputField = hasCursor && fields.some(
1961
- (f) => f.is_input && !f.is_non_display && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
2060
+ (f) => f.is_input && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
1962
2061
  );
1963
2062
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
1964
2063
  rows.map((line, index) => {
@@ -2168,6 +2267,14 @@ function GreenScreenTerminal({
2168
2267
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : connStatus?.status === "connecting" ? "Connecting..." : "Disconnected" }),
2169
2268
  connStatus.error && !isAutoReconnecting && !reconnecting && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: "0.75em", opacity: 0.7, maxWidth: "80%", textAlign: "center", wordBreak: "break-word" }, children: connStatus.error })
2170
2269
  ] }),
2270
+ showBusyOverlay && connStatus?.connected && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-busy-overlay", role: "status", "aria-live": "polite", children: [
2271
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RefreshIcon, { size: 22, className: "gs-spin" }),
2272
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "gs-busy-clock", children: [
2273
+ "X CLOCK\xA0\xA0",
2274
+ formatBusyClock(lockElapsedMs)
2275
+ ] }),
2276
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-busy-hint", children: "Waiting for host\u2026" })
2277
+ ] }),
2171
2278
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2172
2279
  "input",
2173
2280
  {