@contentful/experience-design-system-cli 2.7.0 → 2.7.1-dev-build-02830eb.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.
Files changed (34) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/analyze/select/tui/App.js +54 -475
  3. package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +3 -5
  4. package/dist/src/analyze/select/tui/components/ComponentDetail.js +3 -3
  5. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +1 -1
  6. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +2 -10
  7. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +1 -1
  8. package/dist/src/analyze/select/tui/components/HelpOverlay.js +2 -7
  9. package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +7 -5
  10. package/dist/src/analyze/select/tui/components/JsonEditor.js +15 -146
  11. package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +1 -1
  12. package/dist/src/analyze/select/tui/components/QuitDialog.js +2 -10
  13. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -2
  14. package/dist/src/analyze/select/tui/components/StatusBar.d.ts +0 -2
  15. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +5 -0
  16. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +7 -4
  17. package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +0 -23
  18. package/dist/src/analyze/select/tui/hooks/useKeymap.js +1 -67
  19. package/dist/src/analyze/select/tui/hooks/useRawMode.d.ts +6 -0
  20. package/dist/src/analyze/select/tui/hooks/useRawMode.js +16 -0
  21. package/dist/src/analyze/select/tui/hooks/useSideEffects.d.ts +17 -0
  22. package/dist/src/analyze/select/tui/hooks/useSideEffects.js +226 -0
  23. package/dist/src/analyze/select/tui/hooks/useUndo.js +15 -21
  24. package/dist/src/analyze/select/tui/inputToAction.d.ts +13 -0
  25. package/dist/src/analyze/select/tui/inputToAction.js +68 -0
  26. package/dist/src/analyze/select/tui/state.d.ts +136 -0
  27. package/dist/src/analyze/select/tui/state.js +385 -0
  28. package/dist/src/analyze/select/tui/utils.d.ts +5 -0
  29. package/dist/src/analyze/select/tui/utils.js +11 -0
  30. package/dist/src/analyze/tui/AnalyzeView.js +2 -0
  31. package/dist/src/import/tui/WizardApp.js +2 -0
  32. package/dist/src/import/tui/steps/GenerateReviewStep.js +4 -11
  33. package/dist/src/print/validate/tui/ValidateView.js +2 -0
  34. package/package.json +2 -2
@@ -1,15 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { useImmediateInput } from '../hooks/useImmediateInput.js';
4
- export function FinalizeDialog({ accepted, rejected, needsReview, onConfirm, onCancel, }) {
5
- useImmediateInput((input, key) => {
6
- if (input === 'y' || key.return) {
7
- onConfirm();
8
- }
9
- else if (input === 'n' || key.escape) {
10
- onCancel();
11
- }
12
- });
3
+ // Pure render input handled by inputToAction in App.
4
+ export function FinalizeDialog({ accepted, rejected, needsReview, onConfirm: _onConfirm, onCancel: _onCancel, }) {
13
5
  const allResolved = needsReview === 0;
14
6
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", padding: 1, width: 58, children: [_jsx(Text, { bold: true, children: '─'.repeat(17) + ' Finalize ' + '─'.repeat(17) }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsxs(Text, { color: "green", children: [accepted, " accepted"] }), _jsx(Text, { children: " \u00B7 " }), _jsxs(Text, { color: "red", children: [rejected, " rejected"] }), _jsx(Text, { children: " \u00B7 " }), _jsxs(Text, { dimColor: true, children: [needsReview, " unresolved"] })] }), _jsx(Text, { children: " " }), !allResolved && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: '⚠ ' + needsReview + ' component' + (needsReview === 1 ? ' is' : 's are') + ' unresolved and will be' }), _jsx(Text, { color: "yellow", children: ' excluded from the output.' }), _jsx(Text, { children: " " })] })), _jsx(Text, { children: allResolved ? 'Save decisions and exit? All components resolved.' : 'Save decisions and exit?' }), _jsx(Text, { children: " " }), _jsx(Text, { children: ' [y / Enter] Confirm [n / Esc] Cancel' })] }));
15
7
  }
@@ -3,5 +3,5 @@ type HelpOverlayProps = {
3
3
  mode: 'analyze' | 'validate' | 'review';
4
4
  onClose: () => void;
5
5
  };
6
- export declare function HelpOverlay({ mode, onClose }: HelpOverlayProps): React.ReactElement;
6
+ export declare function HelpOverlay({ mode, onClose: _onClose }: HelpOverlayProps): React.ReactElement;
7
7
  export {};
@@ -1,11 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { useImmediateInput } from '../hooks/useImmediateInput.js';
4
- export function HelpOverlay({ mode, onClose }) {
5
- useImmediateInput((input, key) => {
6
- if (input === '?' || key.escape) {
7
- onClose();
8
- }
9
- });
3
+ // Pure render input handled by inputToAction in App.
4
+ export function HelpOverlay({ mode, onClose: _onClose }) {
10
5
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", padding: 1, width: 46, children: [_jsx(Text, { bold: true, children: '─'.repeat(18) + ' Help ' + '─'.repeat(18) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Navigation" }), _jsx(Text, { children: ' ↑ / k / PgUp Scroll up' }), _jsx(Text, { children: ' ↓ / j / PgDn Scroll down' }), _jsx(Text, { children: ' g / Home Jump to top' }), _jsx(Text, { children: ' G / End Jump to bottom' }), mode === 'review' && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "[Review mode only]" }), _jsx(Text, { children: ' Tab Toggle sidebar/detail' }), _jsx(Text, { children: ' a Accept component' }), _jsx(Text, { children: ' r Reject component' }), _jsx(Text, { children: ' e Edit proposal' }), _jsx(Text, { children: ' s Toggle source code' }), _jsx(Text, { children: ' A Approve all' }), _jsx(Text, { children: ' F Open finalize dialog' }), _jsx(Text, { children: ' Ctrl+S Save draft' }), _jsx(Text, { children: ' Ctrl+Z Undo' }), _jsx(Text, { children: ' Esc Exit edit / close' })] })), _jsx(Text, { children: " " }), _jsx(Text, { children: ' ? Close help' }), _jsx(Text, { children: ' q Quit' })] }));
11
6
  }
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
+ import type { EditorState } from '../state.js';
2
3
  type JsonEditorProps = {
3
- value: string;
4
+ editorState: EditorState;
4
5
  width: number;
5
6
  height: number;
6
- onChange: (value: string) => void;
7
- onSave: () => void;
8
- onDiscard: () => void;
9
7
  };
10
- export declare function JsonEditor({ value, width, height, onChange, onSave, onDiscard }: JsonEditorProps): React.ReactElement;
8
+ /**
9
+ * Pure render component — no input handling, no hooks.
10
+ * All state lives in the reducer; all keys are handled by inputToAction.
11
+ */
12
+ export declare function JsonEditor({ editorState, width, height }: JsonEditorProps): React.ReactElement;
11
13
  export {};
@@ -1,154 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
2
  import { Box, Text } from 'ink';
4
- import { useUndo } from '../hooks/useUndo.js';
5
- import { useImmediateInput } from '../hooks/useImmediateInput.js';
6
- export function JsonEditor({ value, width, height, onChange, onSave, onDiscard }) {
7
- const undo = useUndo({
8
- lines: value.split('\n'),
9
- cursorRow: 0,
10
- cursorCol: 0,
11
- });
12
- const [scrollRow, setScrollRow] = useState(0);
13
- const [scrollCol] = useState(0);
14
- const [validationError, setValidationError] = useState(null);
15
- const [cursorVisible, setCursorVisible] = useState(true);
16
- // Blinking cursor
17
- useEffect(() => {
18
- const interval = setInterval(() => setCursorVisible((v) => !v), 500);
19
- return () => clearInterval(interval);
20
- }, []);
21
- const { lines, cursorRow, cursorCol } = undo.current;
22
- // Keep scroll in sync with cursor
23
- useEffect(() => {
24
- if (cursorRow < scrollRow)
25
- setScrollRow(cursorRow);
26
- if (cursorRow >= scrollRow + height)
27
- setScrollRow(cursorRow - height + 1);
28
- }, [cursorRow, scrollRow, height]);
29
- useImmediateInput((input, key) => {
30
- const currentLines = undo.current.lines;
31
- const currentRow = undo.current.cursorRow;
32
- const currentCol = undo.current.cursorCol;
33
- if (key.ctrl && input === 's') {
34
- // Validate and save
35
- const text = currentLines.join('\n');
36
- try {
37
- JSON.parse(text);
38
- setValidationError(null);
39
- onChange(text);
40
- onSave();
41
- }
42
- catch (e) {
43
- setValidationError(e instanceof Error ? e.message : String(e));
44
- }
45
- return;
46
- }
47
- if (key.escape) {
48
- onDiscard();
49
- return;
50
- }
51
- if (key.ctrl && input === 'z') {
52
- undo.undo();
53
- onChange(undo.current.lines.join('\n'));
54
- return;
55
- }
56
- let newLines = [...currentLines];
57
- let newRow = currentRow;
58
- let newCol = currentCol;
59
- if (key.return) {
60
- const before = newLines[currentRow].slice(0, currentCol);
61
- const after = newLines[currentRow].slice(currentCol);
62
- newLines = [...newLines.slice(0, currentRow), before, after, ...newLines.slice(currentRow + 1)];
63
- newRow = currentRow + 1;
64
- newCol = 0;
65
- }
66
- else if (key.backspace || key.delete) {
67
- if (key.backspace) {
68
- if (currentCol > 0) {
69
- newLines[currentRow] = newLines[currentRow].slice(0, currentCol - 1) + newLines[currentRow].slice(currentCol);
70
- newCol = currentCol - 1;
71
- }
72
- else if (currentRow > 0) {
73
- const prevLen = newLines[currentRow - 1].length;
74
- newLines[currentRow - 1] = newLines[currentRow - 1] + newLines[currentRow];
75
- newLines = [...newLines.slice(0, currentRow), ...newLines.slice(currentRow + 1)];
76
- newRow = currentRow - 1;
77
- newCol = prevLen;
78
- }
79
- }
80
- else {
81
- // Delete key
82
- if (currentCol < newLines[currentRow].length) {
83
- newLines[currentRow] = newLines[currentRow].slice(0, currentCol) + newLines[currentRow].slice(currentCol + 1);
84
- }
85
- else if (currentRow < newLines.length - 1) {
86
- newLines[currentRow] = newLines[currentRow] + newLines[currentRow + 1];
87
- newLines = [...newLines.slice(0, currentRow + 1), ...newLines.slice(currentRow + 2)];
88
- }
89
- }
90
- }
91
- else if (key.leftArrow) {
92
- if (currentCol > 0) {
93
- newCol = currentCol - 1;
94
- }
95
- else if (currentRow > 0) {
96
- newRow = currentRow - 1;
97
- newCol = newLines[currentRow - 1].length;
98
- }
99
- }
100
- else if (key.rightArrow) {
101
- if (currentCol < newLines[currentRow].length) {
102
- newCol = currentCol + 1;
103
- }
104
- else if (currentRow < newLines.length - 1) {
105
- newRow = currentRow + 1;
106
- newCol = 0;
107
- }
108
- }
109
- else if (key.upArrow) {
110
- if (currentRow > 0) {
111
- newRow = currentRow - 1;
112
- newCol = Math.min(currentCol, newLines[currentRow - 1].length);
113
- }
114
- }
115
- else if (key.downArrow) {
116
- if (currentRow < newLines.length - 1) {
117
- newRow = currentRow + 1;
118
- newCol = Math.min(currentCol, newLines[currentRow + 1].length);
119
- }
120
- }
121
- else if (input === '\x1b[H' || input === '\x1b[1~') {
122
- // Home key
123
- newCol = 0;
124
- }
125
- else if (input === '\x1b[F' || input === '\x1b[4~') {
126
- // End key
127
- newCol = newLines[currentRow].length;
128
- }
129
- else if (input && input.length === 1 && !key.ctrl && !key.meta) {
130
- // Printable character
131
- newLines[currentRow] = newLines[currentRow].slice(0, currentCol) + input + newLines[currentRow].slice(currentCol);
132
- newCol = currentCol + 1;
133
- }
134
- else {
135
- return; // No change
136
- }
137
- undo.push({ lines: newLines, cursorRow: newRow, cursorCol: newCol });
138
- onChange(newLines.join('\n'));
139
- });
3
+ /**
4
+ * Pure render component — no input handling, no hooks.
5
+ * All state lives in the reducer; all keys are handled by inputToAction.
6
+ */
7
+ export function JsonEditor({ editorState, width, height }) {
8
+ const { cursor, scrollRow, validationError } = editorState;
9
+ const { lines, cursorRow, cursorCol } = cursor;
140
10
  const innerWidth = Math.max(1, width - 2);
141
11
  const visibleLines = lines.slice(scrollRow, scrollRow + height);
142
- return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: "white", children: [_jsx(Text, { bold: true, children: 'EDIT [EDITING — Ctrl+S save · Esc discard]' }), visibleLines.map((line, displayRow) => {
12
+ return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: "white", children: [_jsx(Text, { bold: true, children: 'EDIT [Ctrl+S save · Ctrl+Z undo · Esc discard]' }), visibleLines.map((line, displayRow) => {
143
13
  const actualRow = displayRow + scrollRow;
144
- const displayLine = line.slice(scrollCol, scrollCol + innerWidth);
14
+ const displayLine = line.slice(0, innerWidth);
145
15
  if (actualRow === cursorRow) {
146
- // Render cursor on this line
147
- const beforeCursor = displayLine.slice(0, cursorCol - scrollCol);
148
- const cursorChar = displayLine[cursorCol - scrollCol] ?? ' ';
149
- const afterCursor = displayLine.slice(cursorCol - scrollCol + 1);
150
- return (_jsxs(Box, { children: [_jsx(Text, { children: beforeCursor }), _jsx(Text, { inverse: cursorVisible, children: cursorChar }), _jsx(Text, { children: afterCursor })] }, displayRow));
16
+ const before = displayLine.slice(0, cursorCol);
17
+ const cursorChar = displayLine[cursorCol] ?? ' ';
18
+ const after = displayLine.slice(cursorCol + 1);
19
+ return (_jsxs(Box, { children: [_jsx(Text, { children: before }), _jsx(Text, { inverse: true, children: cursorChar }), _jsx(Text, { children: after })] }, actualRow));
151
20
  }
152
- return _jsx(Text, { children: displayLine }, displayRow);
153
- }), validationError && _jsx(Text, { color: "red", children: '✗ Invalid JSON: ' + validationError })] }));
21
+ return _jsx(Text, { children: displayLine }, actualRow);
22
+ }), validationError && _jsx(Text, { color: "red", children: '✗ ' + validationError })] }));
154
23
  }
@@ -4,5 +4,5 @@ type QuitDialogProps = {
4
4
  onConfirm: () => void;
5
5
  onCancel: () => void;
6
6
  };
7
- export declare function QuitDialog({ hasUnsavedDrafts, onConfirm, onCancel }: QuitDialogProps): React.ReactElement;
7
+ export declare function QuitDialog({ hasUnsavedDrafts, onConfirm: _onConfirm, onCancel: _onCancel, }: QuitDialogProps): React.ReactElement;
8
8
  export {};
@@ -1,14 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { useImmediateInput } from '../hooks/useImmediateInput.js';
4
- export function QuitDialog({ hasUnsavedDrafts, onConfirm, onCancel }) {
5
- useImmediateInput((input, key) => {
6
- if (input === 'y' || key.return) {
7
- onConfirm();
8
- }
9
- else if (input === 'n' || key.escape) {
10
- onCancel();
11
- }
12
- });
3
+ // Pure render input handled by inputToAction in App.
4
+ export function QuitDialog({ hasUnsavedDrafts, onConfirm: _onConfirm, onCancel: _onCancel, }) {
13
5
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", padding: 1, width: 50, children: [_jsx(Text, { bold: true, children: '─'.repeat(19) + ' Quit ' + '─'.repeat(19) }), _jsx(Text, { children: " " }), hasUnsavedDrafts ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: "You have unsaved draft edits." }), _jsx(Text, { children: 'Session state is preserved — you can resume' }), _jsx(Text, { children: 'by running the same review command again.' })] })) : (_jsx(Text, { children: "Session is saved. Quit without finalizing?" })), _jsx(Text, { children: " " }), _jsx(Text, { children: ' [y / Enter] Quit [n / Esc] Stay' })] }));
14
6
  }
@@ -6,8 +6,6 @@ type SidebarProps = {
6
6
  focused: boolean;
7
7
  scrollOffset: number;
8
8
  visibleCount: number;
9
- onSelect: (id: string) => void;
10
- onScrollChange: (offset: number) => void;
11
9
  collapsed?: boolean;
12
10
  width?: number;
13
11
  };
@@ -4,8 +4,6 @@ type StatusBarProps = {
4
4
  rejected: number;
5
5
  reviewed: number;
6
6
  needsReview: number;
7
- onApproveAll: () => void;
8
- onFinalize: () => void;
9
7
  };
10
8
  export declare function StatusBar({ accepted, rejected, reviewed, needsReview }: StatusBarProps): React.ReactElement;
11
9
  export {};
@@ -19,6 +19,11 @@ type InputHandler = (input: string, key: Key) => void;
19
19
  * Like Ink's useInput, but uses useLayoutEffect so the listener is registered
20
20
  * synchronously after render. This allows stdin.write() calls in tests to work
21
21
  * immediately after render() without awaiting effects.
22
+ *
23
+ * Raw mode is NOT toggled here — it is managed once at the App level via
24
+ * useRawMode. Toggling setRawMode per-hook caused brief raw-mode drops during
25
+ * re-renders (e.g. when FieldEditor mounted alongside useKeymap), blanking the
26
+ * terminal.
22
27
  */
23
28
  export declare function useImmediateInput(handler: InputHandler): void;
24
29
  export {};
@@ -47,13 +47,17 @@ function parseInput(data) {
47
47
  * Like Ink's useInput, but uses useLayoutEffect so the listener is registered
48
48
  * synchronously after render. This allows stdin.write() calls in tests to work
49
49
  * immediately after render() without awaiting effects.
50
+ *
51
+ * Raw mode is NOT toggled here — it is managed once at the App level via
52
+ * useRawMode. Toggling setRawMode per-hook caused brief raw-mode drops during
53
+ * re-renders (e.g. when FieldEditor mounted alongside useKeymap), blanking the
54
+ * terminal.
50
55
  */
51
56
  export function useImmediateInput(handler) {
52
- const { stdin, setRawMode } = useStdin();
57
+ const { stdin } = useStdin();
53
58
  const handlerRef = useRef(handler);
54
59
  handlerRef.current = handler;
55
60
  useLayoutEffect(() => {
56
- setRawMode(true);
57
61
  const handleData = (data) => {
58
62
  const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
59
63
  const { input, key } = parseInput(str);
@@ -62,7 +66,6 @@ export function useImmediateInput(handler) {
62
66
  stdin.on('data', handleData);
63
67
  return () => {
64
68
  stdin.off('data', handleData);
65
- setRawMode(false);
66
69
  };
67
- }, [stdin, setRawMode]);
70
+ }, []); // Register once on mount. Handler is always fresh via handlerRef — no re-registration needed.
68
71
  }
@@ -1,24 +1 @@
1
- type KeymapContext = {
2
- sidebarFocused: boolean;
3
- editMode: boolean;
4
- dialogOpen: boolean;
5
- disabled: boolean;
6
- };
7
- type KeymapHandlers = {
8
- onSidebarUp: () => void;
9
- onSidebarDown: () => void;
10
- onSidebarSelect: () => void;
11
- onAccept: () => void;
12
- onReject: () => void;
13
- onEnterEditMode: () => void;
14
- onToggleSource: () => void;
15
- onScrollUp: () => void;
16
- onScrollDown: () => void;
17
- onToggleFocus: () => void;
18
- onApproveAll: () => void;
19
- onFinalize: () => void;
20
- onQuit: () => void;
21
- onToggleHelp: () => void;
22
- };
23
- export declare function useKeymap(context: KeymapContext, handlers: KeymapHandlers): void;
24
1
  export {};
@@ -1,67 +1 @@
1
- import { useImmediateInput } from './useImmediateInput.js';
2
- export function useKeymap(context, handlers) {
3
- useImmediateInput((input, key) => {
4
- if (context.disabled)
5
- return;
6
- if (context.dialogOpen)
7
- return;
8
- if (context.editMode)
9
- return;
10
- if (input === 'q') {
11
- handlers.onQuit();
12
- return;
13
- }
14
- if (input === '?') {
15
- handlers.onToggleHelp();
16
- return;
17
- }
18
- if (key.tab) {
19
- handlers.onToggleFocus();
20
- return;
21
- }
22
- if (input === 'A') {
23
- handlers.onApproveAll();
24
- return;
25
- }
26
- if (input === 'F') {
27
- handlers.onFinalize();
28
- return;
29
- }
30
- // a/r/e/s work regardless of sidebar focus
31
- if (input === 'a') {
32
- handlers.onAccept();
33
- return;
34
- }
35
- if (input === 'r') {
36
- handlers.onReject();
37
- return;
38
- }
39
- if (input === 'e') {
40
- handlers.onEnterEditMode();
41
- return;
42
- }
43
- if (input === 's') {
44
- handlers.onToggleSource();
45
- return;
46
- }
47
- if (context.sidebarFocused) {
48
- if (key.upArrow || input === 'k') {
49
- handlers.onSidebarUp();
50
- }
51
- else if (key.downArrow || input === 'j') {
52
- handlers.onSidebarDown();
53
- }
54
- else if (key.return) {
55
- handlers.onSidebarSelect();
56
- }
57
- }
58
- else {
59
- if (key.upArrow || input === 'k') {
60
- handlers.onScrollUp();
61
- }
62
- else if (key.downArrow || input === 'j') {
63
- handlers.onScrollDown();
64
- }
65
- }
66
- });
67
- }
1
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Enables raw terminal mode for the lifetime of the component.
3
+ * Call once at the root of each top-level TUI component.
4
+ * useImmediateInput does NOT manage raw mode — this hook does.
5
+ */
6
+ export declare function useRawMode(): void;
@@ -0,0 +1,16 @@
1
+ import { useLayoutEffect } from 'react';
2
+ import { useStdin } from 'ink';
3
+ /**
4
+ * Enables raw terminal mode for the lifetime of the component.
5
+ * Call once at the root of each top-level TUI component.
6
+ * useImmediateInput does NOT manage raw mode — this hook does.
7
+ */
8
+ export function useRawMode() {
9
+ const { setRawMode } = useStdin();
10
+ useLayoutEffect(() => {
11
+ setRawMode(true);
12
+ return () => {
13
+ setRawMode(false);
14
+ };
15
+ }, [setRawMode]);
16
+ }
@@ -0,0 +1,17 @@
1
+ import type { AppState, AppAction } from '../state.js';
2
+ import type { ReviewSessionSnapshot } from '../../types.js';
3
+ type Services = {
4
+ sessionId: string;
5
+ saveState: (session: ReviewSessionSnapshot) => Promise<void>;
6
+ appendEvent: (event: {
7
+ type: string;
8
+ payload: Record<string, unknown>;
9
+ }) => Promise<void>;
10
+ };
11
+ type Dispatch = (action: AppAction) => void;
12
+ /**
13
+ * All async side effects in one place.
14
+ * Detects transitions by comparing state to prevState — no pending* counter signals needed.
15
+ */
16
+ export declare function useSideEffects(state: AppState, dispatch: Dispatch, services: Services): void;
17
+ export {};