@contentful/experience-design-system-cli 2.7.1-dev-build-02830eb.0 → 2.7.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.
Files changed (34) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/analyze/select/tui/App.js +475 -54
  3. package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +5 -3
  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 +10 -2
  7. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +1 -1
  8. package/dist/src/analyze/select/tui/components/HelpOverlay.js +7 -2
  9. package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +5 -7
  10. package/dist/src/analyze/select/tui/components/JsonEditor.js +146 -15
  11. package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +1 -1
  12. package/dist/src/analyze/select/tui/components/QuitDialog.js +10 -2
  13. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +2 -0
  14. package/dist/src/analyze/select/tui/components/StatusBar.d.ts +2 -0
  15. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +0 -5
  16. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +4 -7
  17. package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +23 -0
  18. package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -1
  19. package/dist/src/analyze/select/tui/hooks/useUndo.js +21 -15
  20. package/dist/src/analyze/tui/AnalyzeView.js +0 -2
  21. package/dist/src/import/tui/WizardApp.js +0 -2
  22. package/dist/src/import/tui/steps/GenerateReviewStep.js +11 -4
  23. package/dist/src/print/validate/tui/ValidateView.js +0 -2
  24. package/package.json +2 -2
  25. package/dist/src/analyze/select/tui/hooks/useRawMode.d.ts +0 -6
  26. package/dist/src/analyze/select/tui/hooks/useRawMode.js +0 -16
  27. package/dist/src/analyze/select/tui/hooks/useSideEffects.d.ts +0 -17
  28. package/dist/src/analyze/select/tui/hooks/useSideEffects.js +0 -226
  29. package/dist/src/analyze/select/tui/inputToAction.d.ts +0 -13
  30. package/dist/src/analyze/select/tui/inputToAction.js +0 -68
  31. package/dist/src/analyze/select/tui/state.d.ts +0 -136
  32. package/dist/src/analyze/select/tui/state.js +0 -385
  33. package/dist/src/analyze/select/tui/utils.d.ts +0 -5
  34. package/dist/src/analyze/select/tui/utils.js +0 -11
@@ -1,7 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- // Pure render input handled by inputToAction in App.
4
- export function FinalizeDialog({ accepted, rejected, needsReview, onConfirm: _onConfirm, onCancel: _onCancel, }) {
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
+ });
5
13
  const allResolved = needsReview === 0;
6
14
  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' })] }));
7
15
  }
@@ -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: _onClose }: HelpOverlayProps): React.ReactElement;
6
+ export declare function HelpOverlay({ mode, onClose }: HelpOverlayProps): React.ReactElement;
7
7
  export {};
@@ -1,6 +1,11 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- // Pure render input handled by inputToAction in App.
4
- export function HelpOverlay({ mode, onClose: _onClose }) {
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
+ });
5
10
  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' })] }));
6
11
  }
@@ -1,13 +1,11 @@
1
1
  import React from 'react';
2
- import type { EditorState } from '../state.js';
3
2
  type JsonEditorProps = {
4
- editorState: EditorState;
3
+ value: string;
5
4
  width: number;
6
5
  height: number;
6
+ onChange: (value: string) => void;
7
+ onSave: () => void;
8
+ onDiscard: () => void;
7
9
  };
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;
10
+ export declare function JsonEditor({ value, width, height, onChange, onSave, onDiscard }: JsonEditorProps): React.ReactElement;
13
11
  export {};
@@ -1,23 +1,154 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
2
3
  import { Box, Text } from 'ink';
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;
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
+ });
10
140
  const innerWidth = Math.max(1, width - 2);
11
141
  const visibleLines = lines.slice(scrollRow, scrollRow + height);
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) => {
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) => {
13
143
  const actualRow = displayRow + scrollRow;
14
- const displayLine = line.slice(0, innerWidth);
144
+ const displayLine = line.slice(scrollCol, scrollCol + innerWidth);
15
145
  if (actualRow === cursorRow) {
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));
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));
20
151
  }
21
- return _jsx(Text, { children: displayLine }, actualRow);
22
- }), validationError && _jsx(Text, { color: "red", children: '✗ ' + validationError })] }));
152
+ return _jsx(Text, { children: displayLine }, displayRow);
153
+ }), validationError && _jsx(Text, { color: "red", children: '✗ Invalid JSON: ' + validationError })] }));
23
154
  }
@@ -4,5 +4,5 @@ type QuitDialogProps = {
4
4
  onConfirm: () => void;
5
5
  onCancel: () => void;
6
6
  };
7
- export declare function QuitDialog({ hasUnsavedDrafts, onConfirm: _onConfirm, onCancel: _onCancel, }: QuitDialogProps): React.ReactElement;
7
+ export declare function QuitDialog({ hasUnsavedDrafts, onConfirm, onCancel }: QuitDialogProps): React.ReactElement;
8
8
  export {};
@@ -1,6 +1,14 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- // Pure render input handled by inputToAction in App.
4
- export function QuitDialog({ hasUnsavedDrafts, onConfirm: _onConfirm, onCancel: _onCancel, }) {
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
+ });
5
13
  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' })] }));
6
14
  }
@@ -6,6 +6,8 @@ type SidebarProps = {
6
6
  focused: boolean;
7
7
  scrollOffset: number;
8
8
  visibleCount: number;
9
+ onSelect: (id: string) => void;
10
+ onScrollChange: (offset: number) => void;
9
11
  collapsed?: boolean;
10
12
  width?: number;
11
13
  };
@@ -4,6 +4,8 @@ type StatusBarProps = {
4
4
  rejected: number;
5
5
  reviewed: number;
6
6
  needsReview: number;
7
+ onApproveAll: () => void;
8
+ onFinalize: () => void;
7
9
  };
8
10
  export declare function StatusBar({ accepted, rejected, reviewed, needsReview }: StatusBarProps): React.ReactElement;
9
11
  export {};
@@ -19,11 +19,6 @@ 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.
27
22
  */
28
23
  export declare function useImmediateInput(handler: InputHandler): void;
29
24
  export {};
@@ -47,17 +47,13 @@ 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.
55
50
  */
56
51
  export function useImmediateInput(handler) {
57
- const { stdin } = useStdin();
52
+ const { stdin, setRawMode } = useStdin();
58
53
  const handlerRef = useRef(handler);
59
54
  handlerRef.current = handler;
60
55
  useLayoutEffect(() => {
56
+ setRawMode(true);
61
57
  const handleData = (data) => {
62
58
  const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
63
59
  const { input, key } = parseInput(str);
@@ -66,6 +62,7 @@ export function useImmediateInput(handler) {
66
62
  stdin.on('data', handleData);
67
63
  return () => {
68
64
  stdin.off('data', handleData);
65
+ setRawMode(false);
69
66
  };
70
- }, []); // Register once on mount. Handler is always fresh via handlerRef — no re-registration needed.
67
+ }, [stdin, setRawMode]);
71
68
  }
@@ -1 +1,24 @@
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;
1
24
  export {};
@@ -1 +1,67 @@
1
- export {};
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,20 +1,26 @@
1
1
  import { useState } from 'react';
2
- // Single-state implementation — push and undo are atomic single setState calls,
3
- // eliminating the double-render that caused flash in the JsonEditor.
4
2
  export function useUndo(initial, maxSize = 50) {
5
- const [state, setState] = useState({ stack: [], current: initial });
3
+ const [stack, setStack] = useState([]);
4
+ const [current, setCurrent] = useState(initial);
6
5
  return {
7
- current: state.current,
8
- push: (next) => setState(({ stack, current }) => {
9
- const newStack = [...stack, current];
10
- return { stack: newStack.length > maxSize ? newStack.slice(1) : newStack, current: next };
11
- }),
12
- undo: () => setState(({ stack, current }) => {
13
- if (stack.length === 0)
14
- return { stack, current };
15
- return { stack: stack.slice(0, -1), current: stack[stack.length - 1] };
16
- }),
17
- canUndo: state.stack.length > 0,
18
- clear: () => setState(({ current }) => ({ stack: [], current })),
6
+ current,
7
+ push: (next) => {
8
+ setStack((prev) => {
9
+ const newStack = [...prev, current];
10
+ return newStack.length > maxSize ? newStack.slice(1) : newStack;
11
+ });
12
+ setCurrent(next);
13
+ },
14
+ undo: () => {
15
+ setStack((prev) => {
16
+ if (prev.length === 0)
17
+ return prev;
18
+ const newStack = prev.slice(0, -1);
19
+ setCurrent(prev[prev.length - 1]);
20
+ return newStack;
21
+ });
22
+ },
23
+ canUndo: stack.length > 0,
24
+ clear: () => setStack([]),
19
25
  };
20
26
  }
@@ -3,14 +3,12 @@ import { useState } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
4
  import { TopBar } from '../select/tui/components/TopBar.js';
5
5
  import { useImmediateInput } from '../select/tui/hooks/useImmediateInput.js';
6
- import { useRawMode } from '../select/tui/hooks/useRawMode.js';
7
6
  function truncateName(name, maxLen = 30) {
8
7
  if (name.length <= maxLen)
9
8
  return name;
10
9
  return name.slice(0, maxLen - 1) + '…';
11
10
  }
12
11
  export function AnalyzeView({ result, onExit }) {
13
- useRawMode();
14
12
  const [scrollOffset, setScrollOffset] = useState(0);
15
13
  const { stdout } = useStdout();
16
14
  // Header lines: TopBar(1) + summary(3) + blank(1) + dividers+header(3) + blank(1) + footer(1) + footer-bar(1) = ~12
@@ -8,7 +8,6 @@ import { homedir, tmpdir } from 'node:os';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { execFile, spawn } from 'node:child_process';
10
10
  import { TopBar } from '../../analyze/select/tui/components/TopBar.js';
11
- import { useRawMode } from '../../analyze/select/tui/hooks/useRawMode.js';
12
11
  import { WelcomeStep } from './steps/WelcomeStep.js';
13
12
  import { PathValidationStep } from './steps/PathValidationStep.js';
14
13
  import { RunningStep } from './steps/RunningStep.js';
@@ -45,7 +44,6 @@ function logStep(entry) {
45
44
  export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master', initialCmaToken = '', initialHost, initialAgent, initialProjectPath, host, } = {}) {
46
45
  const defaultConfiguredHost = toConfiguredHost(host || process.env['EDS_HOST']) ?? DEFAULT_CONFIGURED_HOST;
47
46
  const resolveWizardHost = (hostValue) => hostValue || defaultConfiguredHost;
48
- useRawMode();
49
47
  const { stdout } = useStdout();
50
48
  const terminalWidth = stdout?.columns ?? 80;
51
49
  const logInit = useRef(false);
@@ -8,7 +8,6 @@ import { StatusBar } from '../../../analyze/select/tui/components/StatusBar.js';
8
8
  import { FinalizeDialog } from '../../../analyze/select/tui/components/FinalizeDialog.js';
9
9
  import { QuitDialog } from '../../../analyze/select/tui/components/QuitDialog.js';
10
10
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
11
- import { computeScrollOffset } from '../../../analyze/select/tui/utils.js';
12
11
  import { openPipelineDb, loadCDFComponents, storeCDFComponents } from '../../../session/db.js';
13
12
  const VISIBLE_COUNT = 20;
14
13
  const PANEL_HEIGHT = 22;
@@ -162,7 +161,7 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
162
161
  const newIdx = Math.min(components.length - 1, selectedIdx + 1);
163
162
  setSelectedIdx(newIdx);
164
163
  setJsonScrollOffset(0);
165
- setSidebarScrollOffset((prev) => computeScrollOffset(newIdx, prev, VISIBLE_COUNT));
164
+ setSidebarScrollOffset((prev) => (newIdx >= prev + VISIBLE_COUNT ? newIdx - VISIBLE_COUNT + 1 : prev));
166
165
  }
167
166
  }
168
167
  else {
@@ -197,7 +196,15 @@ export function GenerateReviewStep({ extractSessionId, onFinalize, onQuit, }) {
197
196
  const needsReview = components.filter((c) => c.status === 'needs-review').length;
198
197
  const propCount = selected ? Object.keys(selected.entry.$properties).length : 0;
199
198
  const slotCount = selected?.entry.$slots ? Object.keys(selected.entry.$slots).length : 0;
200
- return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, flexDirection: "column", children: selected ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: selected.key }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [propCount, " prop", propCount !== 1 ? 's' : '', slotCount > 0 ? ` · ${slotCount} slot${slotCount !== 1 ? 's' : ''}` : '', ' ', sidebarFocused ? '[Tab] focus panel' : '[Tab] focus list'] })] }), editMode ? (_jsx(FieldEditor, { value: draftValue || selectedJson, width: panelWidth, height: PANEL_HEIGHT, onChange: setDraftValue, onSave: handleEditSave, onDiscard: handleEditDiscard })) : (_jsx(JsonPanel, { label: "GENERATED DEFINITION", value: selectedJson, scrollOffset: jsonScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: !sidebarFocused })), saveError && _jsx(Text, { color: "red", children: '✗ ' + saveError }), _jsx(Text, { dimColor: true, children: editMode
199
+ return (_jsxs(Box, { flexDirection: "column", children: [showFinalize && (_jsx(FinalizeDialog, { accepted: accepted, rejected: rejected, needsReview: needsReview, onConfirm: handleFinalizeConfirm, onCancel: () => setShowFinalize(false) })), showQuit && _jsx(QuitDialog, { hasUnsavedDrafts: false, onConfirm: onQuit, onCancel: () => setShowQuit(false) }), !dialogOpen && (_jsxs(Box, { children: [_jsx(Sidebar, { components: sidebarItems, selectedId: selected?.key ?? null, focused: sidebarFocused, scrollOffset: sidebarScrollOffset, visibleCount: VISIBLE_COUNT, onSelect: (id) => {
200
+ const idx = components.findIndex((c) => c.key === id);
201
+ if (idx >= 0) {
202
+ setSelectedIdx(idx);
203
+ setJsonScrollOffset(0);
204
+ }
205
+ }, onScrollChange: setSidebarScrollOffset, width: sidebarWidth }), _jsx(Box, { flexGrow: 1, paddingLeft: 1, flexDirection: "column", children: selected ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: selected.key }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: [propCount, " prop", propCount !== 1 ? 's' : '', slotCount > 0 ? ` · ${slotCount} slot${slotCount !== 1 ? 's' : ''}` : '', ' ', sidebarFocused ? '[Tab] focus panel' : '[Tab] focus list'] })] }), editMode ? (_jsx(FieldEditor, { value: draftValue || selectedJson, width: panelWidth, height: PANEL_HEIGHT, onChange: setDraftValue, onSave: handleEditSave, onDiscard: handleEditDiscard })) : (_jsx(JsonPanel, { label: "GENERATED DEFINITION", value: selectedJson, scrollOffset: jsonScrollOffset, width: panelWidth, height: PANEL_HEIGHT, active: !sidebarFocused })), saveError && _jsx(Text, { color: "red", children: '✗ ' + saveError }), _jsx(Text, { dimColor: true, children: editMode
201
206
  ? ' [Ctrl+S] save [Esc] discard'
202
- : ' [a] accept [r] reject [e] edit [A] accept all [F] finalize [Tab] toggle focus [q] quit' })] })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), !dialogOpen && _jsx(StatusBar, { accepted: accepted, rejected: rejected, reviewed: 0, needsReview: needsReview })] }));
207
+ : ' [a] accept [r] reject [e] edit [A] accept all [F] finalize [Tab] toggle focus [q] quit' })] })) : (_jsx(Text, { dimColor: true, children: "No component selected" })) })] })), !dialogOpen && (_jsx(StatusBar, { accepted: accepted, rejected: rejected, reviewed: 0, needsReview: needsReview, onApproveAll: () => {
208
+ setComponents((prev) => prev.map((c) => (c.status === 'needs-review' ? { ...c, status: 'accepted' } : c)));
209
+ }, onFinalize: () => setShowFinalize(true) }))] }));
203
210
  }
@@ -3,9 +3,7 @@ import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { TopBar } from '../../../analyze/select/tui/components/TopBar.js';
5
5
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
6
- import { useRawMode } from '../../../analyze/select/tui/hooks/useRawMode.js';
7
6
  export function ValidateView({ results, onExit }) {
8
- useRawMode();
9
7
  const [scrollOffset, setScrollOffset] = useState(0);
10
8
  const allValid = results.every((r) => r.valid);
11
9
  useImmediateInput((input, key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.7.1-dev-build-02830eb.0",
3
+ "version": "2.7.1",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "react-dom": "^18.3.1",
37
37
  "ts-morph": "^27.0.2",
38
38
  "typescript": "^5.9.3",
39
- "@contentful/experience-design-system-types": "2.7.1-dev-build-02830eb.0"
39
+ "@contentful/experience-design-system-types": "2.7.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@tsconfig/node24": "^24.0.3",
@@ -1,6 +0,0 @@
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;
@@ -1,16 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
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 {};