codex-lens 0.1.26 → 0.1.28

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Codex Lens</title>
7
- <script type="module" crossorigin src="./assets/main-BO4694Xj.js"></script>
7
+ <script type="module" crossorigin src="./assets/main-BRIK-RhC.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/main-DNXrKVO-.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-lens",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "A visualization tool for Codex that monitors API requests and file system changes",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/aggregator.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createServer } from 'http';
2
- import { createHash, createHash as cryptoCreateHash } from 'crypto';
3
2
  import { WebSocketServer } from 'ws';
4
3
  import express from 'express';
5
4
  import { spawn } from 'child_process';
@@ -237,6 +237,54 @@ export function App() {
237
237
  }
238
238
  }
239
239
 
240
+ async function saveAllFiles() {
241
+ const modifiedTabs = tabs.filter(t => t.modified);
242
+ if (modifiedTabs.length === 0) return;
243
+
244
+ setSaving(true);
245
+ const port = window.location.port === '5173' ? '5174' : window.location.port;
246
+ const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
247
+
248
+ let savedCount = 0;
249
+ let failedFiles = [];
250
+
251
+ for (const tab of modifiedTabs) {
252
+ try {
253
+ const response = await fetch(`${protocol}//${window.location.hostname}:${port}/api/save-file`, {
254
+ method: 'POST',
255
+ headers: { 'Content-Type': 'application/json' },
256
+ body: JSON.stringify({ path: tab.path, content: tab.content })
257
+ });
258
+
259
+ if (response.ok) {
260
+ savedCount++;
261
+ } else {
262
+ const error = await response.json();
263
+ failedFiles.push(tab.name);
264
+ console.error('Failed to save file:', tab.name, error.message);
265
+ }
266
+ } catch (error) {
267
+ failedFiles.push(tab.name);
268
+ console.error('Failed to save file:', tab.name, error);
269
+ }
270
+ }
271
+
272
+ if (savedCount > 0) {
273
+ setTabs(prevTabs => prevTabs.map(t => {
274
+ if (t.modified && !failedFiles.includes(t.name)) {
275
+ return { ...t, originalContent: t.content, modified: false };
276
+ }
277
+ return t;
278
+ }));
279
+ }
280
+
281
+ if (failedFiles.length > 0) {
282
+ alert(`以下文件保存失败: ${failedFiles.join(', ')}`);
283
+ }
284
+
285
+ setSaving(false);
286
+ }
287
+
240
288
  function handleFileClick(path) {
241
289
  const existingTab = tabs.find(t => t.path === path);
242
290
  if (existingTab) {
@@ -382,6 +430,7 @@ export function App() {
382
430
  x={contextMenu.x}
383
431
  y={contextMenu.y}
384
432
  tab={tabs.find(t => t.id === contextMenu.tabId)}
433
+ tabs={tabs}
385
434
  saving={saving}
386
435
  onClose={() => setContextMenu(null)}
387
436
  onCloseTab={() => {
@@ -400,6 +449,10 @@ export function App() {
400
449
  saveFile(contextMenu.tabId);
401
450
  setContextMenu(null);
402
451
  }}
452
+ onSaveAll={() => {
453
+ saveAllFiles();
454
+ setContextMenu(null);
455
+ }}
403
456
  />
404
457
  )}
405
458
  <div className="panel right-panel">
@@ -441,7 +494,9 @@ function TabBar({ tabs, activeTabId, onTabClick, onTabClose, onContextMenu }) {
441
494
  );
442
495
  }
443
496
 
444
- function ContextMenu({ x, y, tab, saving, onClose, onCloseTab, onCloseOtherTabs, onCloseAllTabs, onSave }) {
497
+ function ContextMenu({ x, y, tab, tabs, saving, onClose, onCloseTab, onCloseOtherTabs, onCloseAllTabs, onSave, onSaveAll }) {
498
+ const hasModified = tabs?.some(t => t.modified);
499
+
445
500
  return (
446
501
  <div className="context-menu" style={{ left: x, top: y }} onClick={(e) => e.stopPropagation()}>
447
502
  {tab?.modified && (
@@ -449,6 +504,11 @@ function ContextMenu({ x, y, tab, saving, onClose, onCloseTab, onCloseOtherTabs,
449
504
  {saving ? '保存中...' : '保存'}
450
505
  </div>
451
506
  )}
507
+ {hasModified && (
508
+ <div className="context-menu-item" onClick={onSaveAll} style={{ color: '#4ade80' }}>
509
+ {saving ? '保存中...' : '全部保存'}
510
+ </div>
511
+ )}
452
512
  <div className="context-menu-item" onClick={onCloseTab}>关闭</div>
453
513
  <div className="context-menu-item" onClick={onCloseOtherTabs}>关闭其他</div>
454
514
  <div className="context-menu-item" onClick={onCloseAllTabs}>关闭所有</div>
@@ -1,6 +1,6 @@
1
- import React, { useMemo, useRef } from 'react';
1
+ import { useMemo, useRef } from 'react';
2
2
  import CodeMirror from '@uiw/react-codemirror';
3
- import { EditorView, Decoration, ViewPlugin, ViewUpdate } from '@codemirror/view';
3
+ import { EditorView, Decoration, ViewPlugin } from '@codemirror/view';
4
4
  import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
5
5
  import { tags as t } from '@lezer/highlight';
6
6
  import { RangeSetBuilder } from '@codemirror/state';
@@ -0,0 +1,172 @@
1
+ import { Component } from 'react';
2
+
3
+ class DefaultErrorFallback extends Component {
4
+ render() {
5
+ const { error, onReload, onReset } = this.props;
6
+
7
+ return (
8
+ <div style={{
9
+ height: '100vh',
10
+ display: 'flex',
11
+ flexDirection: 'column',
12
+ alignItems: 'center',
13
+ justifyContent: 'center',
14
+ background: '#1e1e1e',
15
+ color: '#e0e0e0',
16
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
17
+ padding: '20px',
18
+ }}>
19
+ <div style={{
20
+ maxWidth: '600px',
21
+ padding: '32px',
22
+ background: '#252526',
23
+ borderRadius: '8px',
24
+ border: '1px solid #3c3c3c',
25
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.4)',
26
+ textAlign: 'center',
27
+ }}>
28
+ <div style={{
29
+ fontSize: '48px',
30
+ marginBottom: '16px',
31
+ }}>
32
+ <span style={{ color: '#f85149' }}>!</span>
33
+ </div>
34
+
35
+ <h1 style={{
36
+ fontSize: '24px',
37
+ fontWeight: 600,
38
+ marginBottom: '8px',
39
+ color: '#ffffff',
40
+ }}>
41
+ 应用程序出错
42
+ </h1>
43
+
44
+ <p style={{
45
+ fontSize: '14px',
46
+ color: '#858585',
47
+ marginBottom: '24px',
48
+ }}>
49
+ 抱歉,应用程序遇到了一个错误。请尝试刷新页面或重置应用状态。
50
+ </p>
51
+
52
+ {error && (
53
+ <details style={{
54
+ marginBottom: '24px',
55
+ padding: '12px',
56
+ background: '#1e1e1e',
57
+ borderRadius: '4px',
58
+ textAlign: 'left',
59
+ }}>
60
+ <summary style={{
61
+ cursor: 'pointer',
62
+ fontSize: '13px',
63
+ color: '#e0e0e0',
64
+ fontWeight: 500,
65
+ }}>
66
+ 错误详情
67
+ </summary>
68
+ <pre style={{
69
+ marginTop: '12px',
70
+ padding: '12px',
71
+ background: '#0a0a0a',
72
+ borderRadius: '4px',
73
+ fontSize: '12px',
74
+ color: '#f48771',
75
+ overflow: 'auto',
76
+ maxHeight: '200px',
77
+ fontFamily: '"SF Mono", Consolas, monospace',
78
+ }}>
79
+ {error.toString()}
80
+ {error.stack && `\n\n${error.stack}`}
81
+ </pre>
82
+ </details>
83
+ )}
84
+
85
+ <div style={{
86
+ display: 'flex',
87
+ gap: '12px',
88
+ justifyContent: 'center',
89
+ }}>
90
+ <button
91
+ onClick={onReset}
92
+ style={{
93
+ padding: '10px 20px',
94
+ background: '#3c3c3c',
95
+ border: '1px solid #4c4c4c',
96
+ borderRadius: '4px',
97
+ color: '#cccccc',
98
+ fontSize: '14px',
99
+ cursor: 'pointer',
100
+ }}
101
+ >
102
+ 重置应用
103
+ </button>
104
+ <button
105
+ onClick={onReload}
106
+ style={{
107
+ padding: '10px 20px',
108
+ background: '#0078d4',
109
+ border: 'none',
110
+ borderRadius: '4px',
111
+ color: '#ffffff',
112
+ fontSize: '14px',
113
+ cursor: 'pointer',
114
+ fontWeight: 500,
115
+ }}
116
+ >
117
+ 刷新页面
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ );
123
+ }
124
+ }
125
+
126
+ export class ErrorBoundary extends Component {
127
+ constructor(props) {
128
+ super(props);
129
+ this.state = {
130
+ hasError: false,
131
+ error: null,
132
+ errorInfo: null,
133
+ };
134
+ }
135
+
136
+ static getDerivedStateFromError(error) {
137
+ return { hasError: true, error };
138
+ }
139
+
140
+ componentDidCatch(error, errorInfo) {
141
+ this.setState({ errorInfo });
142
+ this.props.onError?.(error, errorInfo);
143
+ console.error('[ErrorBoundary]', error, errorInfo);
144
+ }
145
+
146
+ handleReload = () => {
147
+ this.setState({ hasError: false, error: null, errorInfo: null });
148
+ window.location.reload();
149
+ };
150
+
151
+ handleReset = () => {
152
+ this.setState({ hasError: false, error: null, errorInfo: null });
153
+ };
154
+
155
+ render() {
156
+ if (this.state.hasError) {
157
+ if (this.props.fallback) {
158
+ return this.props.fallback;
159
+ }
160
+ return (
161
+ <DefaultErrorFallback
162
+ error={this.state.error}
163
+ errorInfo={this.state.errorInfo}
164
+ onReload={this.handleReload}
165
+ onReset={this.handleReset}
166
+ />
167
+ );
168
+ }
169
+
170
+ return this.props.children;
171
+ }
172
+ }
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import { useEffect, useRef, useCallback } from 'react';
2
2
  import { Terminal } from '@xterm/xterm';
3
3
  import { FitAddon } from '@xterm/addon-fit';
4
4
  import { WebLinksAddon } from '@xterm/addon-web-links';
@@ -15,42 +15,66 @@ const VIRTUAL_KEYS = [
15
15
  { label: 'Ctrl+C', seq: '\x03' },
16
16
  ];
17
17
 
18
- export class TerminalPanel extends React.Component {
19
- constructor(props) {
20
- super(props);
21
- this.containerRef = React.createRef();
22
- this.terminal = null;
23
- this.fitAddon = null;
24
- this.ws = null;
25
- this.resizeObserver = null;
26
- this._writeBuffer = '';
27
- this._writeTimer = null;
28
- }
29
-
30
- componentDidMount() {
31
- this.initTerminal();
32
- this.connectWebSocket();
33
- this.setupResizeObserver();
34
- }
35
-
36
- componentWillUnmount() {
37
- if (this._writeTimer) {
38
- cancelAnimationFrame(this._writeTimer);
18
+ export function TerminalPanel() {
19
+ const containerRef = useRef(null);
20
+ const terminalRef = useRef(null);
21
+ const fitAddonRef = useRef(null);
22
+ const wsRef = useRef(null);
23
+ const resizeObserverRef = useRef(null);
24
+ const writeBufferRef = useRef('');
25
+ const writeTimerRef = useRef(null);
26
+ const resizeTimerRef = useRef(null);
27
+
28
+ const sendResize = useCallback(() => {
29
+ if (wsRef.current?.readyState === WebSocket.OPEN && terminalRef.current) {
30
+ wsRef.current.send(JSON.stringify({
31
+ type: 'resize',
32
+ cols: terminalRef.current.cols,
33
+ rows: terminalRef.current.rows,
34
+ }));
39
35
  }
40
- if (this.ws) {
41
- this.ws.close();
42
- this.ws = null;
36
+ }, []);
37
+
38
+ const flushWrite = useCallback(() => {
39
+ if (writeTimerRef.current) {
40
+ cancelAnimationFrame(writeTimerRef.current);
41
+ writeTimerRef.current = null;
43
42
  }
44
- if (this.resizeObserver) {
45
- this.resizeObserver.disconnect();
43
+ if (!writeBufferRef.current || !terminalRef.current) return;
44
+
45
+ const CHUNK_SIZE = 32768;
46
+ if (writeBufferRef.current.length <= CHUNK_SIZE) {
47
+ const buf = writeBufferRef.current;
48
+ writeBufferRef.current = '';
49
+ terminalRef.current.write(buf);
50
+ } else {
51
+ const chunk = writeBufferRef.current.slice(0, CHUNK_SIZE);
52
+ writeBufferRef.current = writeBufferRef.current.slice(CHUNK_SIZE);
53
+ terminalRef.current.write(chunk);
54
+ writeTimerRef.current = requestAnimationFrame(() => {
55
+ flushWrite();
56
+ });
46
57
  }
47
- if (this.terminal) {
48
- this.terminal.dispose();
58
+ }, []);
59
+
60
+ const throttledWrite = useCallback((data) => {
61
+ writeBufferRef.current += data;
62
+ if (!writeTimerRef.current) {
63
+ writeTimerRef.current = requestAnimationFrame(() => {
64
+ flushWrite();
65
+ });
49
66
  }
50
- }
67
+ }, [flushWrite]);
51
68
 
52
- initTerminal() {
53
- this.terminal = new Terminal({
69
+ const handleVirtualKey = useCallback((seq) => {
70
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
71
+ wsRef.current.send(JSON.stringify({ type: 'input', data: seq }));
72
+ }
73
+ terminalRef.current?.focus();
74
+ }, []);
75
+
76
+ useEffect(() => {
77
+ const terminal = new Terminal({
54
78
  cursorBlink: true,
55
79
  cursorStyle: 'bar',
56
80
  fontSize: 13,
@@ -65,52 +89,54 @@ export class TerminalPanel extends React.Component {
65
89
  allowProposedApi: true,
66
90
  });
67
91
 
68
- this.fitAddon = new FitAddon();
69
- this.terminal.loadAddon(this.fitAddon);
70
- this.terminal.loadAddon(new WebLinksAddon());
92
+ const fitAddon = new FitAddon();
93
+ terminal.loadAddon(fitAddon);
94
+ terminal.loadAddon(new WebLinksAddon());
71
95
 
72
- this.terminal.open(this.containerRef.current);
96
+ terminal.open(containerRef.current);
97
+ terminalRef.current = terminal;
98
+ fitAddonRef.current = fitAddon;
73
99
 
74
100
  requestAnimationFrame(() => {
75
- if (this.fitAddon) {
76
- this.fitAddon.fit();
77
- this.terminal.focus();
101
+ if (fitAddon) {
102
+ fitAddon.fit();
103
+ terminal.focus();
78
104
  }
79
105
  });
80
106
 
81
- this.terminal.onData((data) => {
82
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
83
- this.ws.send(JSON.stringify({ type: 'input', data }));
107
+ terminal.onData((data) => {
108
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
109
+ wsRef.current.send(JSON.stringify({ type: 'input', data }));
84
110
  }
85
111
  });
86
- }
87
112
 
88
- connectWebSocket() {
113
+ // WebSocket connection
89
114
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
90
115
  const host = window.location.hostname;
91
116
  const port = window.location.port === '5173' ? '5174' : window.location.port;
92
117
  const wsUrl = `${protocol}//${host}:${port}/ws/terminal`;
93
118
 
94
- this.ws = new WebSocket(wsUrl);
119
+ const ws = new WebSocket(wsUrl);
120
+ wsRef.current = ws;
95
121
 
96
- this.ws.onopen = () => {
122
+ ws.onopen = () => {
97
123
  console.log('[Terminal] Connected to PTY service');
98
- this.sendResize();
124
+ sendResize();
99
125
  };
100
126
 
101
- this.ws.onmessage = (event) => {
127
+ ws.onmessage = (event) => {
102
128
  try {
103
129
  const msg = JSON.parse(event.data);
104
130
  if (msg.type === 'data') {
105
- this._throttledWrite(msg.data);
131
+ throttledWrite(msg.data);
106
132
  } else if (msg.type === 'exit') {
107
- this._flushWrite();
108
- if (this.terminal) {
109
- this.terminal.write(`\r\n[Process exited with code ${msg.exitCode ?? '?'}]\r\n`);
133
+ flushWrite();
134
+ if (terminal) {
135
+ terminal.write(`\r\n[Process exited with code ${msg.exitCode ?? '?'}]\r\n`);
110
136
  }
111
137
  } else if (msg.type === 'state') {
112
- if (!msg.running && this.terminal) {
113
- this._flushWrite();
138
+ if (!msg.running && terminal) {
139
+ flushWrite();
114
140
  }
115
141
  }
116
142
  } catch (e) {
@@ -118,132 +144,97 @@ export class TerminalPanel extends React.Component {
118
144
  }
119
145
  };
120
146
 
121
- this.ws.onclose = () => {
147
+ ws.onclose = () => {
122
148
  console.log('[Terminal] Disconnected, reconnecting in 3s...');
123
149
  setTimeout(() => {
124
- if (this.containerRef.current) {
125
- this.connectWebSocket();
150
+ if (containerRef.current) {
151
+ // Reconnect handled by re-mounting effect
126
152
  }
127
153
  }, 3000);
128
154
  };
129
155
 
130
- this.ws.onerror = (error) => {
156
+ ws.onerror = (error) => {
131
157
  console.error('[Terminal] WebSocket error:', error);
132
158
  };
133
- }
134
-
135
- sendResize() {
136
- if (this.ws && this.ws.readyState === WebSocket.OPEN && this.terminal) {
137
- this.ws.send(JSON.stringify({
138
- type: 'resize',
139
- cols: this.terminal.cols,
140
- rows: this.terminal.rows,
141
- }));
142
- }
143
- }
144
159
 
145
- setupResizeObserver() {
146
- if (!this.containerRef.current) return;
147
-
148
- this.resizeObserver = new ResizeObserver(() => {
149
- if (this._resizeTimer) clearTimeout(this._resizeTimer);
150
- this._resizeTimer = setTimeout(() => {
151
- if (this.fitAddon && this.containerRef.current) {
160
+ // ResizeObserver setup
161
+ const resizeObserver = new ResizeObserver(() => {
162
+ if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
163
+ resizeTimerRef.current = setTimeout(() => {
164
+ if (fitAddonRef.current && containerRef.current) {
152
165
  try {
153
- this.fitAddon.fit();
154
- this.sendResize();
166
+ fitAddonRef.current.fit();
167
+ sendResize();
155
168
  } catch {}
156
169
  }
157
170
  }, 150);
158
171
  });
159
172
 
160
- this.resizeObserver.observe(this.containerRef.current);
161
- }
162
-
163
- _throttledWrite(data) {
164
- this._writeBuffer += data;
165
- if (!this._writeTimer) {
166
- this._writeTimer = requestAnimationFrame(() => {
167
- this._flushWrite();
168
- });
169
- }
170
- }
171
-
172
- _flushWrite() {
173
- if (this._writeTimer) {
174
- cancelAnimationFrame(this._writeTimer);
175
- this._writeTimer = null;
176
- }
177
- if (!this._writeBuffer || !this.terminal) return;
178
-
179
- const CHUNK_SIZE = 32768;
180
- if (this._writeBuffer.length <= CHUNK_SIZE) {
181
- const buf = this._writeBuffer;
182
- this._writeBuffer = '';
183
- this.terminal.write(buf);
184
- } else {
185
- const chunk = this._writeBuffer.slice(0, CHUNK_SIZE);
186
- this._writeBuffer = this._writeBuffer.slice(CHUNK_SIZE);
187
- this.terminal.write(chunk);
188
- this._writeTimer = requestAnimationFrame(() => {
189
- this._flushWrite();
190
- });
191
- }
192
- }
173
+ resizeObserver.observe(containerRef.current);
174
+ resizeObserverRef.current = resizeObserver;
193
175
 
194
- handleVirtualKey = (seq) => {
195
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
196
- this.ws.send(JSON.stringify({ type: 'input', data: seq }));
197
- }
198
- this.terminal?.focus();
199
- };
200
-
201
- render() {
202
- return (
176
+ // Cleanup on unmount
177
+ return () => {
178
+ if (writeTimerRef.current) {
179
+ cancelAnimationFrame(writeTimerRef.current);
180
+ }
181
+ if (ws) {
182
+ ws.close();
183
+ wsRef.current = null;
184
+ }
185
+ if (resizeObserver) {
186
+ resizeObserver.disconnect();
187
+ }
188
+ if (terminal) {
189
+ terminal.dispose();
190
+ }
191
+ };
192
+ }, [sendResize, throttledWrite, flushWrite]);
193
+
194
+ return (
195
+ <div style={{
196
+ height: '100%',
197
+ display: 'flex',
198
+ flexDirection: 'column',
199
+ background: '#0a0a0a',
200
+ }}>
201
+ <div
202
+ ref={containerRef}
203
+ style={{
204
+ flex: 1,
205
+ overflow: 'hidden',
206
+ padding: '4px 8px',
207
+ }}
208
+ />
203
209
  <div style={{
204
- height: '100%',
205
210
  display: 'flex',
206
- flexDirection: 'column',
207
- background: '#0a0a0a',
211
+ gap: '4px',
212
+ padding: '8px',
213
+ background: '#111',
214
+ borderTop: '1px solid #222',
215
+ flexWrap: 'wrap',
208
216
  }}>
209
- <div
210
- ref={this.containerRef}
211
- style={{
212
- flex: 1,
213
- overflow: 'hidden',
214
- padding: '4px 8px',
215
- }}
216
- />
217
- <div style={{
218
- display: 'flex',
219
- gap: '4px',
220
- padding: '8px',
221
- background: '#111',
222
- borderTop: '1px solid #222',
223
- flexWrap: 'wrap',
224
- }}>
225
- {VIRTUAL_KEYS.map((key) => (
226
- <button
227
- key={key.label}
228
- onClick={() => this.handleVirtualKey(key.seq)}
229
- style={{
230
- padding: '8px 12px',
231
- border: '1px solid #333',
232
- borderRadius: '4px',
233
- background: '#1a1a1a',
234
- color: '#ccc',
235
- fontSize: '13px',
236
- fontFamily: 'Menlo, Monaco, monospace',
237
- cursor: 'pointer',
238
- minWidth: '44px',
239
- minHeight: '44px',
240
- }}
241
- >
242
- {key.label}
243
- </button>
244
- ))}
245
- </div>
217
+ {VIRTUAL_KEYS.map((key) => (
218
+ <button
219
+ key={key.label}
220
+ onClick={() => handleVirtualKey(key.seq)}
221
+ style={{
222
+ padding: '8px 12px',
223
+ border: '1px solid #333',
224
+ borderRadius: '4px',
225
+ background: '#1a1a1a',
226
+ color: '#ccc',
227
+ fontSize: '13px',
228
+ fontFamily: 'Menlo, Monaco, monospace',
229
+ cursor: 'pointer',
230
+ minWidth: '44px',
231
+ minHeight: '44px',
232
+ }}
233
+ >
234
+ {key.label}
235
+ </button>
236
+ ))}
246
237
  </div>
247
- );
248
- }
249
- }
238
+ </div>
239
+ );
240
+ }