react-retro-display-tty-ansi-ascii 0.1.39 → 0.1.46

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/README.md CHANGED
@@ -45,6 +45,66 @@ That is the whole entry point.
45
45
  You hand the component a mode, a value or controller when needed, and let it handle the grid,
46
46
  wrapping, cursor rendering, and terminal feel.
47
47
 
48
+ ## Touch Input
49
+
50
+ Use `touchInput` when the display itself should behave like a touch surface and report
51
+ grid-aligned cell hits back to the host application.
52
+
53
+ The touch contract is intentionally simple:
54
+
55
+ - the component emits 1-based `row` and `col` coordinates
56
+ - it also reports the measured `rows` and `cols` that were active for that touch
57
+ - input is delivered as a single `down` event for each press
58
+ - long presses do not stream repeated events
59
+ - move events are ignored by default in touch mode
60
+ - the next touch is not accepted until the current press is released
61
+
62
+ That makes it a good fit for grid-driven interfaces such as soft terminals, retro games,
63
+ touch menus, and keypad-like overlays where the host wants to interpret one deliberate press
64
+ at a time.
65
+
66
+ ```tsx
67
+ import { useState } from "react";
68
+ import { RetroScreen } from "react-retro-display-tty-ansi-ascii";
69
+ import "react-retro-display-tty-ansi-ascii/styles.css";
70
+
71
+ export function TouchDemo() {
72
+ const [lastTouch, setLastTouch] = useState("Tap the screen");
73
+
74
+ return (
75
+ <RetroScreen
76
+ mode="terminal"
77
+ gridMode="static"
78
+ rows={12}
79
+ cols={32}
80
+ value={[
81
+ "┌──────────────────────────────┐",
82
+ "│ │",
83
+ "│ TOUCH ME │",
84
+ "│ │",
85
+ "│ ↑ up right → │",
86
+ "│ │",
87
+ "│ ← left down ↓ │",
88
+ "│ │",
89
+ "│ │",
90
+ "│ │",
91
+ "│ │",
92
+ "└──────────────────────────────┘"
93
+ ].join("\\n")}
94
+ touchInput={{
95
+ enabled: true,
96
+ onTouchCell: ({ row, col, rows, cols, pointerType }) => {
97
+ setLastTouch(`${pointerType} @ ${row},${col} inside ${rows}x${cols}`);
98
+ }
99
+ }}
100
+ />
101
+ );
102
+ }
103
+ ```
104
+
105
+ The host remains responsible for deciding what a touch means. RetroScreen only handles the
106
+ overlay, pointer capture, hit testing, and the single-press-until-release behavior.
107
+
48
108
  ## Display Padding
49
109
 
50
110
  Use `displayPadding` when the screen content should sit tighter to the glass or breathe a little
package/dist/index.cjs CHANGED
@@ -3915,6 +3915,10 @@ var joinClassNames2 = (...classNames) => classNames.filter(Boolean).join(" ");
3915
3915
  var useIsomorphicLayoutEffect2 = typeof window === "undefined" ? import_react9.useEffect : import_react9.useLayoutEffect;
3916
3916
  var clampSelection2 = (value, text) => Math.max(0, Math.min(text.length, Number.isFinite(value) ? Math.floor(value) : text.length));
3917
3917
  var isMouseTrackingActive = (snapshot) => snapshot.modes.mouseTrackingMode !== "none" && snapshot.modes.mouseProtocol === "sgr";
3918
+ var shouldHandleRetroTouchPointer = ({
3919
+ enabled,
3920
+ phase
3921
+ }) => Boolean(enabled) && phase === "down";
3918
3922
  var isEditorWordNavigationModifier = (event) => event.altKey || event.ctrlKey || event.metaKey;
3919
3923
  var isEditorSelectAllShortcut = (event) => (event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "a";
3920
3924
  var toTerminalMouseButton = (button) => {
@@ -3958,6 +3962,7 @@ function RetroScreen(props) {
3958
3962
  const editorProps = props.mode === "editor" ? props : null;
3959
3963
  const terminalProps = props.mode === "terminal" ? props : null;
3960
3964
  const promptProps = props.mode === "prompt" ? props : null;
3965
+ const touchInput = props.touchInput;
3961
3966
  const screenRef = (0, import_react9.useRef)(null);
3962
3967
  const viewportRef = (0, import_react9.useRef)(null);
3963
3968
  const probeRef = (0, import_react9.useRef)(null);
@@ -3967,6 +3972,7 @@ function RetroScreen(props) {
3967
3972
  const editorSelectionAnchorRef = (0, import_react9.useRef)(null);
3968
3973
  const editorSelectionDraggingRef = (0, import_react9.useRef)(false);
3969
3974
  const previousEditableValueRef = (0, import_react9.useRef)(valueProps?.value ?? "");
3975
+ const activeTouchPointerIdRef = (0, import_react9.useRef)(null);
3970
3976
  const internalTerminalController = useRetroScreenController({
3971
3977
  rows: staticGridActive ? props.rows : void 0,
3972
3978
  cols: staticGridActive ? props.cols : void 0,
@@ -4590,6 +4596,51 @@ function RetroScreen(props) {
4590
4596
  shiftKey: event.shiftKey
4591
4597
  };
4592
4598
  };
4599
+ const handleTouchOverlayPointer = async (event, phase) => {
4600
+ if (phase === "up") {
4601
+ if (activeTouchPointerIdRef.current === event.pointerId) {
4602
+ activeTouchPointerIdRef.current = null;
4603
+ }
4604
+ try {
4605
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
4606
+ event.currentTarget.releasePointerCapture(event.pointerId);
4607
+ }
4608
+ } catch {
4609
+ }
4610
+ return;
4611
+ }
4612
+ if (!touchInput?.onTouchCell || !shouldHandleRetroTouchPointer({ enabled: touchInput.enabled, phase })) {
4613
+ return;
4614
+ }
4615
+ if (activeTouchPointerIdRef.current !== null && activeTouchPointerIdRef.current !== event.pointerId) {
4616
+ return;
4617
+ }
4618
+ const screenNode = screenRef.current;
4619
+ if (!screenNode) {
4620
+ return;
4621
+ }
4622
+ const { row, col } = getRetroScreenPointerGridPosition({
4623
+ clientX: event.clientX,
4624
+ clientY: event.clientY,
4625
+ rect: screenNode.getBoundingClientRect(),
4626
+ geometry
4627
+ });
4628
+ activeTouchPointerIdRef.current = event.pointerId;
4629
+ try {
4630
+ event.currentTarget.setPointerCapture(event.pointerId);
4631
+ } catch {
4632
+ }
4633
+ event.preventDefault();
4634
+ await touchInput.onTouchCell({
4635
+ row,
4636
+ col,
4637
+ rows: geometry.rows,
4638
+ cols: geometry.cols,
4639
+ phase,
4640
+ pointerType: event.pointerType,
4641
+ buttons: event.buttons
4642
+ });
4643
+ };
4593
4644
  const maybeEmitFocusReport = (focusedState) => {
4594
4645
  if (props.mode !== "terminal" || !captureTerminalFocusReport || !terminalSnapshot.modes.focusReportingMode) {
4595
4646
  return;
@@ -4822,6 +4873,26 @@ function RetroScreen(props) {
4822
4873
  "data-fit-width-content-aspect-ratio": fitWidthLayoutMetrics ? String(fitWidthLayoutMetrics.contentAspectRatio) : void 0,
4823
4874
  children: [
4824
4875
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "retro-screen__bezel", "aria-hidden": "true" }),
4876
+ touchInput?.enabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4877
+ "div",
4878
+ {
4879
+ className: "retro-screen__touch-overlay",
4880
+ "data-testid": touchInput.overlayTestId,
4881
+ "aria-hidden": "true",
4882
+ onPointerCancel: (event) => {
4883
+ void handleTouchOverlayPointer(event, "up");
4884
+ },
4885
+ onPointerDown: (event) => {
4886
+ void handleTouchOverlayPointer(event, "down");
4887
+ },
4888
+ onPointerMove: (event) => {
4889
+ void handleTouchOverlayPointer(event, "move");
4890
+ },
4891
+ onPointerUp: (event) => {
4892
+ void handleTouchOverlayPointer(event, "up");
4893
+ }
4894
+ }
4895
+ ) : null,
4825
4896
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4826
4897
  RetroScreenDisplay,
4827
4898
  {