@contentful/experience-design-system-cli 2.2.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 (165) hide show
  1. package/README.md +532 -0
  2. package/bin/cli.js +58 -0
  3. package/dist/package.json +56 -0
  4. package/dist/src/analyze/command.d.ts +3 -0
  5. package/dist/src/analyze/command.js +175 -0
  6. package/dist/src/analyze/extract/astro.d.ts +5 -0
  7. package/dist/src/analyze/extract/astro.js +280 -0
  8. package/dist/src/analyze/extract/pipeline.d.ts +6 -0
  9. package/dist/src/analyze/extract/pipeline.js +298 -0
  10. package/dist/src/analyze/extract/react.d.ts +2 -0
  11. package/dist/src/analyze/extract/react.js +1949 -0
  12. package/dist/src/analyze/extract/slot-detection.d.ts +35 -0
  13. package/dist/src/analyze/extract/slot-detection.js +101 -0
  14. package/dist/src/analyze/extract/stencil.d.ts +2 -0
  15. package/dist/src/analyze/extract/stencil.js +293 -0
  16. package/dist/src/analyze/extract/tsx-shared.d.ts +8 -0
  17. package/dist/src/analyze/extract/tsx-shared.js +263 -0
  18. package/dist/src/analyze/extract/vue-tsx.d.ts +2 -0
  19. package/dist/src/analyze/extract/vue-tsx.js +498 -0
  20. package/dist/src/analyze/extract/vue.d.ts +5 -0
  21. package/dist/src/analyze/extract/vue.js +647 -0
  22. package/dist/src/analyze/extract/web-components.d.ts +2 -0
  23. package/dist/src/analyze/extract/web-components.js +866 -0
  24. package/dist/src/analyze/pre-classify.d.ts +17 -0
  25. package/dist/src/analyze/pre-classify.js +144 -0
  26. package/dist/src/analyze/select/command.d.ts +2 -0
  27. package/dist/src/analyze/select/command.js +256 -0
  28. package/dist/src/analyze/select/index.d.ts +6 -0
  29. package/dist/src/analyze/select/index.js +5 -0
  30. package/dist/src/analyze/select/parser.d.ts +6 -0
  31. package/dist/src/analyze/select/parser.js +53 -0
  32. package/dist/src/analyze/select/persistence.d.ts +9 -0
  33. package/dist/src/analyze/select/persistence.js +42 -0
  34. package/dist/src/analyze/select/stdout.d.ts +7 -0
  35. package/dist/src/analyze/select/stdout.js +3 -0
  36. package/dist/src/analyze/select/tui/App.d.ts +8 -0
  37. package/dist/src/analyze/select/tui/App.js +491 -0
  38. package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +20 -0
  39. package/dist/src/analyze/select/tui/components/ComponentDetail.js +43 -0
  40. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +11 -0
  41. package/dist/src/analyze/select/tui/components/FieldEditor.js +531 -0
  42. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +10 -0
  43. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +15 -0
  44. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +7 -0
  45. package/dist/src/analyze/select/tui/components/HelpOverlay.js +11 -0
  46. package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +11 -0
  47. package/dist/src/analyze/select/tui/components/JsonEditor.js +154 -0
  48. package/dist/src/analyze/select/tui/components/JsonPanel.d.ts +11 -0
  49. package/dist/src/analyze/select/tui/components/JsonPanel.js +62 -0
  50. package/dist/src/analyze/select/tui/components/PreviewSummaryBar.d.ts +8 -0
  51. package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +29 -0
  52. package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +8 -0
  53. package/dist/src/analyze/select/tui/components/QuitDialog.js +14 -0
  54. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +15 -0
  55. package/dist/src/analyze/select/tui/components/Sidebar.js +48 -0
  56. package/dist/src/analyze/select/tui/components/SourcePanel.d.ts +11 -0
  57. package/dist/src/analyze/select/tui/components/SourcePanel.js +52 -0
  58. package/dist/src/analyze/select/tui/components/StatusBar.d.ts +11 -0
  59. package/dist/src/analyze/select/tui/components/StatusBar.js +6 -0
  60. package/dist/src/analyze/select/tui/components/TopBar.d.ts +10 -0
  61. package/dist/src/analyze/select/tui/components/TopBar.js +5 -0
  62. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +24 -0
  63. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +68 -0
  64. package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +24 -0
  65. package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -0
  66. package/dist/src/analyze/select/tui/hooks/useSession.d.ts +19 -0
  67. package/dist/src/analyze/select/tui/hooks/useSession.js +52 -0
  68. package/dist/src/analyze/select/tui/hooks/useUndo.d.ts +8 -0
  69. package/dist/src/analyze/select/tui/hooks/useUndo.js +26 -0
  70. package/dist/src/analyze/select/types.d.ts +46 -0
  71. package/dist/src/analyze/select/types.js +20 -0
  72. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  73. package/dist/src/analyze/select-agent/command.js +208 -0
  74. package/dist/src/analyze/tui/AnalyzeView.d.ts +24 -0
  75. package/dist/src/analyze/tui/AnalyzeView.js +38 -0
  76. package/dist/src/apply/api-client.d.ts +35 -0
  77. package/dist/src/apply/api-client.js +143 -0
  78. package/dist/src/apply/command.d.ts +6 -0
  79. package/dist/src/apply/command.js +787 -0
  80. package/dist/src/apply/manifest.d.ts +1 -0
  81. package/dist/src/apply/manifest.js +1 -0
  82. package/dist/src/apply/tui/SelectView.d.ts +18 -0
  83. package/dist/src/apply/tui/SelectView.js +34 -0
  84. package/dist/src/apply/tui/ServerApplyView.d.ts +32 -0
  85. package/dist/src/apply/tui/ServerApplyView.js +42 -0
  86. package/dist/src/apply/tui/ServerPreviewView.d.ts +9 -0
  87. package/dist/src/apply/tui/ServerPreviewView.js +21 -0
  88. package/dist/src/credentials-store.d.ts +8 -0
  89. package/dist/src/credentials-store.js +30 -0
  90. package/dist/src/generate/agent-runner.d.ts +86 -0
  91. package/dist/src/generate/agent-runner.js +314 -0
  92. package/dist/src/generate/command.d.ts +2 -0
  93. package/dist/src/generate/command.js +545 -0
  94. package/dist/src/generate/edit/command.d.ts +2 -0
  95. package/dist/src/generate/edit/command.js +126 -0
  96. package/dist/src/generate/prompt-builder.d.ts +18 -0
  97. package/dist/src/generate/prompt-builder.js +202 -0
  98. package/dist/src/generate/tui/GenerateView.d.ts +12 -0
  99. package/dist/src/generate/tui/GenerateView.js +10 -0
  100. package/dist/src/import/command.d.ts +2 -0
  101. package/dist/src/import/command.js +96 -0
  102. package/dist/src/import/orchestrator.d.ts +37 -0
  103. package/dist/src/import/orchestrator.js +374 -0
  104. package/dist/src/import/path-utils.d.ts +15 -0
  105. package/dist/src/import/path-utils.js +30 -0
  106. package/dist/src/import/tui/WizardApp.d.ts +10 -0
  107. package/dist/src/import/tui/WizardApp.js +906 -0
  108. package/dist/src/import/tui/steps/CredentialsStep.d.ts +15 -0
  109. package/dist/src/import/tui/steps/CredentialsStep.js +79 -0
  110. package/dist/src/import/tui/steps/DoneStep.d.ts +20 -0
  111. package/dist/src/import/tui/steps/DoneStep.js +17 -0
  112. package/dist/src/import/tui/steps/ErrorStep.d.ts +8 -0
  113. package/dist/src/import/tui/steps/ErrorStep.js +11 -0
  114. package/dist/src/import/tui/steps/GateStep.d.ts +14 -0
  115. package/dist/src/import/tui/steps/GateStep.js +20 -0
  116. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +8 -0
  117. package/dist/src/import/tui/steps/GenerateReviewStep.js +208 -0
  118. package/dist/src/import/tui/steps/PathValidationStep.d.ts +10 -0
  119. package/dist/src/import/tui/steps/PathValidationStep.js +151 -0
  120. package/dist/src/import/tui/steps/PreviewStep.d.ts +21 -0
  121. package/dist/src/import/tui/steps/PreviewStep.js +36 -0
  122. package/dist/src/import/tui/steps/RunningStep.d.ts +10 -0
  123. package/dist/src/import/tui/steps/RunningStep.js +20 -0
  124. package/dist/src/import/tui/steps/TokenInputStep.d.ts +8 -0
  125. package/dist/src/import/tui/steps/TokenInputStep.js +70 -0
  126. package/dist/src/import/tui/steps/WelcomeStep.d.ts +7 -0
  127. package/dist/src/import/tui/steps/WelcomeStep.js +33 -0
  128. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +15 -0
  129. package/dist/src/import/tui/steps/WizardPreviewStep.js +121 -0
  130. package/dist/src/import/tui/steps/preview-diff.d.ts +10 -0
  131. package/dist/src/import/tui/steps/preview-diff.js +132 -0
  132. package/dist/src/index.d.ts +1 -0
  133. package/dist/src/index.js +2 -0
  134. package/dist/src/output/format.d.ts +23 -0
  135. package/dist/src/output/format.js +110 -0
  136. package/dist/src/print/command.d.ts +2 -0
  137. package/dist/src/print/command.js +199 -0
  138. package/dist/src/print/validate/tui/ValidateView.d.ts +15 -0
  139. package/dist/src/print/validate/tui/ValidateView.js +37 -0
  140. package/dist/src/print/validate/validators/cdf-validator.d.ts +2 -0
  141. package/dist/src/print/validate/validators/cdf-validator.js +104 -0
  142. package/dist/src/print/validate/validators/dtcg-validator.d.ts +2 -0
  143. package/dist/src/print/validate/validators/dtcg-validator.js +110 -0
  144. package/dist/src/print/validate/validators/format-errors.d.ts +12 -0
  145. package/dist/src/print/validate/validators/format-errors.js +18 -0
  146. package/dist/src/program.d.ts +2 -0
  147. package/dist/src/program.js +25 -0
  148. package/dist/src/session/command.d.ts +2 -0
  149. package/dist/src/session/command.js +261 -0
  150. package/dist/src/session/db.d.ts +111 -0
  151. package/dist/src/session/db.js +1114 -0
  152. package/dist/src/session/migration.d.ts +4 -0
  153. package/dist/src/session/migration.js +117 -0
  154. package/dist/src/session/session-id.d.ts +1 -0
  155. package/dist/src/session/session-id.js +212 -0
  156. package/dist/src/session/stats.d.ts +27 -0
  157. package/dist/src/session/stats.js +89 -0
  158. package/dist/src/setup/command.d.ts +2 -0
  159. package/dist/src/setup/command.js +765 -0
  160. package/dist/src/types.d.ts +48 -0
  161. package/dist/src/types.js +1 -0
  162. package/package.json +55 -0
  163. package/skills/generate-components.md +361 -0
  164. package/skills/generate-tokens.md +194 -0
  165. package/skills/select-components.md +180 -0
@@ -0,0 +1,154 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ 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
+ });
140
+ const innerWidth = Math.max(1, width - 2);
141
+ 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) => {
143
+ const actualRow = displayRow + scrollRow;
144
+ const displayLine = line.slice(scrollCol, scrollCol + innerWidth);
145
+ 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));
151
+ }
152
+ return _jsx(Text, { children: displayLine }, displayRow);
153
+ }), validationError && _jsx(Text, { color: "red", children: '✗ Invalid JSON: ' + validationError })] }));
154
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ type JsonPanelProps = {
3
+ label: string;
4
+ value: string;
5
+ scrollOffset: number;
6
+ width: number;
7
+ height: number;
8
+ active: boolean;
9
+ };
10
+ export declare function JsonPanel({ label, value, scrollOffset, width, height, active }: JsonPanelProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ function highlightJson(line) {
4
+ // Simple regex-based syntax highlighting
5
+ const parts = [];
6
+ let remaining = line;
7
+ while (remaining.length > 0) {
8
+ // Object key: "key":
9
+ const keyMatch = remaining.match(/^("(?:[^"\\]|\\.)*"\s*:)/);
10
+ if (keyMatch) {
11
+ parts.push({ text: keyMatch[1], color: 'cyan' });
12
+ remaining = remaining.slice(keyMatch[1].length);
13
+ continue;
14
+ }
15
+ // String value
16
+ const strMatch = remaining.match(/^("(?:[^"\\]|\\.)*")/);
17
+ if (strMatch) {
18
+ parts.push({ text: strMatch[1], color: 'green' });
19
+ remaining = remaining.slice(strMatch[1].length);
20
+ continue;
21
+ }
22
+ // Number
23
+ const numMatch = remaining.match(/^(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/);
24
+ if (numMatch) {
25
+ parts.push({ text: numMatch[1], color: 'yellow' });
26
+ remaining = remaining.slice(numMatch[1].length);
27
+ continue;
28
+ }
29
+ // Boolean
30
+ const boolMatch = remaining.match(/^(true|false)/);
31
+ if (boolMatch) {
32
+ parts.push({ text: boolMatch[1], color: 'magenta' });
33
+ remaining = remaining.slice(boolMatch[1].length);
34
+ continue;
35
+ }
36
+ // Null
37
+ const nullMatch = remaining.match(/^(null)/);
38
+ if (nullMatch) {
39
+ parts.push({ text: nullMatch[1], color: 'red', dim: true });
40
+ remaining = remaining.slice(nullMatch[1].length);
41
+ continue;
42
+ }
43
+ // Take one character as-is (punctuation, whitespace, etc.)
44
+ parts.push({ text: remaining[0] });
45
+ remaining = remaining.slice(1);
46
+ }
47
+ return (_jsx(_Fragment, { children: parts.map((part, i) => (_jsx(Text, { color: part.color, dimColor: part.dim, children: part.text }, i))) }));
48
+ }
49
+ function truncateLine(line, maxWidth) {
50
+ if (line.length <= maxWidth)
51
+ return line;
52
+ return line.slice(0, maxWidth - 1) + '…';
53
+ }
54
+ export function JsonPanel({ label, value, scrollOffset, width, height, active }) {
55
+ const allLines = value.split('\n');
56
+ const visibleLines = allLines.slice(scrollOffset, scrollOffset + height);
57
+ const innerWidth = Math.max(1, width - 2); // subtract border
58
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height + 2, borderStyle: "single", borderColor: active ? 'white' : undefined, children: [_jsx(Text, { bold: true, dimColor: !active, children: label }), visibleLines.map((line, i) => {
59
+ const truncated = truncateLine(line, innerWidth);
60
+ return _jsx(Box, { children: highlightJson(truncated) }, i);
61
+ })] }));
62
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
3
+ type PreviewSummaryBarProps = {
4
+ preview: ServerPreviewResponse | null;
5
+ loading: boolean;
6
+ };
7
+ export declare function PreviewSummaryBar({ preview, loading }: PreviewSummaryBarProps): React.ReactElement;
8
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function PreviewSummaryBar({ preview, loading }) {
4
+ if (!preview && !loading) {
5
+ return _jsx(Box, {});
6
+ }
7
+ if (loading && !preview) {
8
+ return (_jsx(Box, { gap: 1, children: _jsx(Text, { dimColor: true, children: "\u21BB Loading preview..." }) }));
9
+ }
10
+ const comp = preview.components;
11
+ const tok = preview.tokens;
12
+ const parts = [];
13
+ const newCount = comp.new.length + tok.new.length;
14
+ const changedCount = comp.changed.length + tok.changed.length;
15
+ const removedCount = comp.removed.length + tok.removed.length;
16
+ const unchangedCount = comp.unchanged.length + tok.unchanged.length;
17
+ if (newCount > 0)
18
+ parts.push({ label: `${newCount} new`, color: 'green' });
19
+ if (changedCount > 0)
20
+ parts.push({ label: `${changedCount} changed`, color: 'yellow' });
21
+ if (removedCount > 0)
22
+ parts.push({ label: `${removedCount} removed`, color: 'red' });
23
+ if (unchangedCount > 0)
24
+ parts.push({ label: `${unchangedCount} unchanged`, color: 'gray' });
25
+ if (parts.length === 0 && !loading) {
26
+ return (_jsx(Box, { gap: 1, children: _jsx(Text, { dimColor: true, children: "Preview: no changes detected" }) }));
27
+ }
28
+ return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Preview:" }), parts.map((part, i) => (_jsx(Text, { color: part.color, children: part.label }, i))), loading && _jsx(Text, { dimColor: true, children: "\u21BB" })] }));
29
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ type QuitDialogProps = {
3
+ hasUnsavedDrafts: boolean;
4
+ onConfirm: () => void;
5
+ onCancel: () => void;
6
+ };
7
+ export declare function QuitDialog({ hasUnsavedDrafts, onConfirm, onCancel }: QuitDialogProps): React.ReactElement;
8
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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
+ });
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' })] }));
14
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import type { ReviewComponentSummary } from '../../types.js';
3
+ type SidebarProps = {
4
+ components: ReviewComponentSummary[];
5
+ selectedId: string | null;
6
+ focused: boolean;
7
+ scrollOffset: number;
8
+ visibleCount: number;
9
+ onSelect: (id: string) => void;
10
+ onScrollChange: (offset: number) => void;
11
+ collapsed?: boolean;
12
+ width?: number;
13
+ };
14
+ export declare function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed, width: widthProp, }: SidebarProps): React.ReactElement;
15
+ export {};
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ function statusIcon(status) {
4
+ switch (status) {
5
+ case 'accepted':
6
+ return '✓';
7
+ case 'rejected':
8
+ return '✗';
9
+ case 'reviewed':
10
+ return '~';
11
+ case 'needs-review':
12
+ return '·';
13
+ }
14
+ }
15
+ function statusColor(status) {
16
+ switch (status) {
17
+ case 'accepted':
18
+ return 'green';
19
+ case 'rejected':
20
+ return 'red';
21
+ case 'reviewed':
22
+ return 'yellow';
23
+ case 'needs-review':
24
+ return 'white';
25
+ }
26
+ }
27
+ function truncateName(name, maxLen) {
28
+ if (name.length <= maxLen)
29
+ return name;
30
+ return name.slice(0, maxLen) + '…';
31
+ }
32
+ export function Sidebar({ components, selectedId, focused, scrollOffset, visibleCount, collapsed = false, width: widthProp, }) {
33
+ const visible = components.slice(scrollOffset, scrollOffset + visibleCount);
34
+ const showScrollUp = scrollOffset > 0;
35
+ const showScrollDown = scrollOffset + visibleCount < components.length;
36
+ const width = collapsed ? 3 : (widthProp ?? 18);
37
+ return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: focused ? 'white' : undefined, children: [showScrollUp && !collapsed && _jsx(Text, { dimColor: true, children: "\u25B2" }), visible.map((component) => {
38
+ const isSelected = component.id === selectedId;
39
+ const icon = statusIcon(component.status);
40
+ const color = statusColor(component.status);
41
+ const maxNameLen = Math.max(1, width - 5);
42
+ const name = truncateName(component.name, maxNameLen);
43
+ if (collapsed) {
44
+ return (_jsx(Box, { children: _jsx(Text, { color: color, inverse: isSelected && focused, underline: isSelected && !focused, children: icon }) }, component.id));
45
+ }
46
+ return (_jsx(Box, { children: _jsx(Text, { color: color, inverse: isSelected && focused, underline: isSelected && !focused, wrap: "truncate", children: icon + ' ' + name }) }, component.id));
47
+ }), showScrollDown && !collapsed && _jsx(Text, { dimColor: true, children: "\u25BC" })] }));
48
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ type SourcePanelProps = {
3
+ sourceCode: string | null;
4
+ filePath: string;
5
+ width: number;
6
+ height: number;
7
+ scrollX: number;
8
+ scrollY: number;
9
+ };
10
+ export declare function SourcePanel({ sourceCode, filePath, width, height, scrollX, scrollY, }: SourcePanelProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const KEYWORDS = /\b(function|export|const|let|import|return|interface|type|default|class|extends|implements|from|async|await)\b/;
4
+ function highlightSourceLine(line) {
5
+ const parts = [];
6
+ let remaining = line;
7
+ while (remaining.length > 0) {
8
+ // String literal
9
+ const strMatch = remaining.match(/^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/);
10
+ if (strMatch) {
11
+ parts.push({ text: strMatch[1], color: 'green' });
12
+ remaining = remaining.slice(strMatch[1].length);
13
+ continue;
14
+ }
15
+ // Keyword
16
+ const kwMatch = remaining.match(KEYWORDS);
17
+ if (kwMatch && kwMatch.index === 0) {
18
+ parts.push({ text: kwMatch[0], color: 'cyan' });
19
+ remaining = remaining.slice(kwMatch[0].length);
20
+ continue;
21
+ }
22
+ parts.push({ text: remaining[0] });
23
+ remaining = remaining.slice(1);
24
+ }
25
+ return (_jsx(_Fragment, { children: parts.map((part, i) => (_jsx(Text, { color: part.color, children: part.text }, i))) }));
26
+ }
27
+ function truncateFilePath(filePath, maxWidth) {
28
+ if (filePath.length <= maxWidth)
29
+ return filePath;
30
+ const filename = filePath.split('/').pop() ?? filePath;
31
+ if (filename.length >= maxWidth)
32
+ return filename.slice(0, maxWidth - 1) + '…';
33
+ return '…' + filePath.slice(filePath.length - (maxWidth - 1));
34
+ }
35
+ export function SourcePanel({ sourceCode, filePath, width, height, scrollX, scrollY, }) {
36
+ const innerWidth = Math.max(1, width - 2);
37
+ const header = truncateFilePath(filePath, innerWidth);
38
+ if (!sourceCode) {
39
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height + 2, borderStyle: "single", children: [_jsx(Text, { dimColor: true, children: header }), _jsx(Box, { justifyContent: "center", alignItems: "center", height: height, children: _jsx(Text, { dimColor: true, children: "[No source available]" }) })] }));
40
+ }
41
+ const allLines = sourceCode.split('\n');
42
+ const visibleLines = allLines.slice(scrollY, scrollY + height);
43
+ const showScrollUp = scrollY > 0;
44
+ const showScrollDown = scrollY + height < allLines.length;
45
+ const maxLineWidth = Math.max(...visibleLines.map((l) => l.length));
46
+ const showScrollLeft = scrollX > 0;
47
+ const showScrollRight = scrollX + innerWidth < maxLineWidth;
48
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height + 2, borderStyle: "single", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: header }), _jsxs(Text, { dimColor: true, children: [showScrollUp ? '▲' : ' ', showScrollDown ? '▼' : ' ', showScrollLeft ? '◀' : ' ', showScrollRight ? '▶' : ' '] })] }), visibleLines.map((line, i) => {
49
+ const sliced = line.slice(scrollX, scrollX + innerWidth);
50
+ return _jsx(Box, { children: highlightSourceLine(sliced) }, i);
51
+ })] }));
52
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ type StatusBarProps = {
3
+ accepted: number;
4
+ rejected: number;
5
+ reviewed: number;
6
+ needsReview: number;
7
+ onApproveAll: () => void;
8
+ onFinalize: () => void;
9
+ };
10
+ export declare function StatusBar({ accepted, rejected, reviewed, needsReview }: StatusBarProps): React.ReactElement;
11
+ export {};
@@ -0,0 +1,6 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function StatusBar({ accepted, rejected, reviewed, needsReview }) {
4
+ const allResolved = needsReview === 0;
5
+ return (_jsxs(Box, { gap: 2, flexWrap: "wrap", children: [_jsxs(Text, { color: "green", children: [accepted, " accepted"] }), _jsxs(Text, { color: "red", children: [rejected, " rejected"] }), _jsxs(Text, { dimColor: true, children: [needsReview, " pending"] }), _jsxs(Text, { color: "cyan", children: [reviewed, " reviewed"] }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: "[A] accept all" }), _jsx(Text, { bold: allResolved, color: allResolved ? 'green' : 'white', children: "[F] finalize" })] }));
6
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ type TopBarProps = {
3
+ subcommand: string;
4
+ hints: Array<{
5
+ key: string;
6
+ label: string;
7
+ }>;
8
+ };
9
+ export declare function TopBar({ subcommand, hints }: TopBarProps): React.ReactElement;
10
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function TopBar({ subcommand, hints }) {
4
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: 'experience-design-system-cli ' + subcommand }), _jsx(Text, { dimColor: true, children: hints.map((h) => `[${h.key}] ${h.label}`).join(' ') })] }));
5
+ }
@@ -0,0 +1,24 @@
1
+ type Key = {
2
+ upArrow: boolean;
3
+ downArrow: boolean;
4
+ leftArrow: boolean;
5
+ rightArrow: boolean;
6
+ pageDown: boolean;
7
+ pageUp: boolean;
8
+ return: boolean;
9
+ escape: boolean;
10
+ ctrl: boolean;
11
+ shift: boolean;
12
+ tab: boolean;
13
+ backspace: boolean;
14
+ delete: boolean;
15
+ meta: boolean;
16
+ };
17
+ type InputHandler = (input: string, key: Key) => void;
18
+ /**
19
+ * Like Ink's useInput, but uses useLayoutEffect so the listener is registered
20
+ * synchronously after render. This allows stdin.write() calls in tests to work
21
+ * immediately after render() without awaiting effects.
22
+ */
23
+ export declare function useImmediateInput(handler: InputHandler): void;
24
+ export {};
@@ -0,0 +1,68 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+ import { useStdin } from 'ink';
3
+ function parseInput(data) {
4
+ const key = {
5
+ upArrow: data === '\x1b[A',
6
+ downArrow: data === '\x1b[B',
7
+ leftArrow: data === '\x1b[D',
8
+ rightArrow: data === '\x1b[C',
9
+ pageDown: data === '\x1b[6~',
10
+ pageUp: data === '\x1b[5~',
11
+ return: data === '\r' || data === '\n',
12
+ escape: data === '\x1b',
13
+ ctrl: false,
14
+ shift: false,
15
+ tab: data === '\t',
16
+ backspace: data === '\x7f' || data === '\b',
17
+ delete: data === '\x1b[3~',
18
+ meta: data === '\x1b',
19
+ };
20
+ let input = data;
21
+ // Ctrl+letter: data is \x01-\x1a (Ctrl+A through Ctrl+Z)
22
+ if (data.length === 1) {
23
+ const code = data.charCodeAt(0);
24
+ if (code >= 1 && code <= 26) {
25
+ key.ctrl = true;
26
+ input = String.fromCharCode(code + 96); // convert to lowercase letter
27
+ }
28
+ }
29
+ // Arrow keys and special keys produce no printable input
30
+ if (key.upArrow ||
31
+ key.downArrow ||
32
+ key.leftArrow ||
33
+ key.rightArrow ||
34
+ key.pageUp ||
35
+ key.pageDown ||
36
+ key.return ||
37
+ key.escape ||
38
+ key.tab ||
39
+ key.backspace ||
40
+ key.delete) {
41
+ if (!key.ctrl)
42
+ input = '';
43
+ }
44
+ return { input, key };
45
+ }
46
+ /**
47
+ * Like Ink's useInput, but uses useLayoutEffect so the listener is registered
48
+ * synchronously after render. This allows stdin.write() calls in tests to work
49
+ * immediately after render() without awaiting effects.
50
+ */
51
+ export function useImmediateInput(handler) {
52
+ const { stdin, setRawMode } = useStdin();
53
+ const handlerRef = useRef(handler);
54
+ handlerRef.current = handler;
55
+ useLayoutEffect(() => {
56
+ setRawMode(true);
57
+ const handleData = (data) => {
58
+ const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
59
+ const { input, key } = parseInput(str);
60
+ handlerRef.current(input, key);
61
+ };
62
+ stdin.on('data', handleData);
63
+ return () => {
64
+ stdin.off('data', handleData);
65
+ setRawMode(false);
66
+ };
67
+ }, [stdin, setRawMode]);
68
+ }
@@ -0,0 +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;
24
+ export {};
@@ -0,0 +1,67 @@
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
+ }