@tangle-network/ui 7.0.0 → 8.1.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.
- package/CHANGELOG.md +12 -0
- package/dist/chat.d.ts +17 -72
- package/dist/chat.js +2 -4
- package/dist/{chunk-QIRVZMQY.js → chunk-C3BIVG72.js} +143 -107
- package/dist/{chunk-5CS3I7Y3.js → chunk-QUAU6ZNC.js} +111 -395
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -9
- package/dist/run.d.ts +30 -2
- package/dist/run.js +4 -6
- package/dist/sdk-hooks.d.ts +1 -1
- package/dist/{tool-call-feed-Bs3MyQMT.d.ts → tool-call-feed-D9iofJgW.d.ts} +1 -23
- package/package.json +2 -2
- package/src/chat/agent-timeline.tsx +139 -45
- package/src/chat/chat-container.tsx +6 -48
- package/src/chat/chat-message.tsx +0 -4
- package/src/chat/index.ts +0 -1
- package/src/markdown/markdown.stories.tsx +1 -1
- package/src/run/assistant-run-shell.tsx +115 -0
- package/src/run/index.ts +5 -1
- package/src/run/run-group.tsx +12 -76
- package/src/run/tool-call-step.tsx +5 -7
- package/src/chat/chat-input.stories.tsx +0 -142
- package/src/chat/chat-input.tsx +0 -389
package/src/chat/chat-input.tsx
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ChatInput — message input bar with file attach, drag-and-drop, send/cancel.
|
|
3
|
-
*
|
|
4
|
-
* - Auto-resizing textarea (up to max height)
|
|
5
|
-
* - Enter to send, Shift+Enter for newline
|
|
6
|
-
* - Drag-and-drop files onto the input with styled overlay
|
|
7
|
-
* - File attachment button (files) + folder attachment button
|
|
8
|
-
* - Pending file/folder chips
|
|
9
|
-
* - Cancel button when streaming
|
|
10
|
-
* - Optional model selector pill
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { useState, useRef, useCallback, type KeyboardEvent, type ChangeEvent, type DragEvent } from "react";
|
|
14
|
-
import { Send, Square, Paperclip, FolderUp, X, Upload } from "lucide-react";
|
|
15
|
-
import { cn } from "../lib/utils";
|
|
16
|
-
|
|
17
|
-
export interface PendingFile {
|
|
18
|
-
id: string;
|
|
19
|
-
name: string;
|
|
20
|
-
size: number;
|
|
21
|
-
type: "file" | "folder";
|
|
22
|
-
/** Number of files inside (for folders) */
|
|
23
|
-
fileCount?: number;
|
|
24
|
-
status: "pending" | "uploading" | "ready" | "error";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ChatInputProps {
|
|
28
|
-
onSend: (message: string, files?: File[]) => void;
|
|
29
|
-
onCancel?: () => void;
|
|
30
|
-
isStreaming?: boolean;
|
|
31
|
-
disabled?: boolean;
|
|
32
|
-
placeholder?: string;
|
|
33
|
-
/** Currently selected model label */
|
|
34
|
-
modelLabel?: string;
|
|
35
|
-
onModelClick?: () => void;
|
|
36
|
-
/** Pending uploaded files */
|
|
37
|
-
pendingFiles?: PendingFile[];
|
|
38
|
-
onRemoveFile?: (id: string) => void;
|
|
39
|
-
/** Called when files are attached (via button or drag-and-drop) */
|
|
40
|
-
onAttach?: (files: FileList) => void;
|
|
41
|
-
/** Called when a folder is selected via the folder button */
|
|
42
|
-
onAttachFolder?: (files: FileList) => void;
|
|
43
|
-
/** Accepted file types for the file input (e.g. ".pdf,.csv") */
|
|
44
|
-
accept?: string;
|
|
45
|
-
/** Drop zone overlay title */
|
|
46
|
-
dropTitle?: string;
|
|
47
|
-
/** Drop zone overlay description */
|
|
48
|
-
dropDescription?: string;
|
|
49
|
-
className?: string;
|
|
50
|
-
/** Label above the input. Set to null to hide. Default: "Agent Command Deck" */
|
|
51
|
-
inputLabel?: string | null;
|
|
52
|
-
/** Status text shown when idle. Set to null to hide. Default: "Ready for next instruction" */
|
|
53
|
-
idleStatus?: string | null;
|
|
54
|
-
/** Status text shown when streaming. Set to null to hide. Default: "Streaming response" */
|
|
55
|
-
streamingStatus?: string | null;
|
|
56
|
-
/** Hide the Cmd+L focus shortcut hint */
|
|
57
|
-
hideShortcutHint?: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function ChatInput({
|
|
61
|
-
onSend,
|
|
62
|
-
onCancel,
|
|
63
|
-
isStreaming,
|
|
64
|
-
disabled,
|
|
65
|
-
placeholder = "Ask the agent to inspect files, run commands, or explain results…",
|
|
66
|
-
modelLabel,
|
|
67
|
-
onModelClick,
|
|
68
|
-
pendingFiles = [],
|
|
69
|
-
onRemoveFile,
|
|
70
|
-
onAttach,
|
|
71
|
-
onAttachFolder,
|
|
72
|
-
accept,
|
|
73
|
-
dropTitle = "Drop files to add context",
|
|
74
|
-
dropDescription = "Files will be attached to your next message.",
|
|
75
|
-
className,
|
|
76
|
-
inputLabel = "Agent Command Deck",
|
|
77
|
-
idleStatus = "Ready for next instruction",
|
|
78
|
-
streamingStatus = "Streaming response",
|
|
79
|
-
hideShortcutHint,
|
|
80
|
-
}: ChatInputProps) {
|
|
81
|
-
const [value, setValue] = useState("");
|
|
82
|
-
const [dragOver, setDragOver] = useState(false);
|
|
83
|
-
const dragCounter = useRef(0);
|
|
84
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
85
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
86
|
-
const folderInputRef = useRef<HTMLInputElement>(null);
|
|
87
|
-
|
|
88
|
-
const handleSend = useCallback(() => {
|
|
89
|
-
const trimmed = value.trim();
|
|
90
|
-
if (!trimmed || isStreaming || disabled) return;
|
|
91
|
-
onSend(trimmed);
|
|
92
|
-
setValue("");
|
|
93
|
-
if (textareaRef.current) {
|
|
94
|
-
textareaRef.current.style.height = "auto";
|
|
95
|
-
}
|
|
96
|
-
}, [value, isStreaming, disabled, onSend]);
|
|
97
|
-
|
|
98
|
-
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
99
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
100
|
-
e.preventDefault();
|
|
101
|
-
handleSend();
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
106
|
-
setValue(e.target.value);
|
|
107
|
-
const el = e.target;
|
|
108
|
-
el.style.height = "auto";
|
|
109
|
-
el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const handleAttachClick = () => {
|
|
113
|
-
fileInputRef.current?.click();
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const handleFolderClick = () => {
|
|
117
|
-
folderInputRef.current?.click();
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
121
|
-
if (e.target.files?.length) {
|
|
122
|
-
onAttach?.(e.target.files);
|
|
123
|
-
e.target.value = "";
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const handleFolderChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
128
|
-
if (e.target.files?.length) {
|
|
129
|
-
(onAttachFolder ?? onAttach)?.(e.target.files);
|
|
130
|
-
e.target.value = "";
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Drag-and-drop handlers
|
|
135
|
-
const handleDragEnter = useCallback((e: DragEvent) => {
|
|
136
|
-
e.preventDefault();
|
|
137
|
-
e.stopPropagation();
|
|
138
|
-
dragCounter.current++;
|
|
139
|
-
if (e.dataTransfer?.types.includes("Files")) {
|
|
140
|
-
setDragOver(true);
|
|
141
|
-
}
|
|
142
|
-
}, []);
|
|
143
|
-
|
|
144
|
-
const handleDragLeave = useCallback((e: DragEvent) => {
|
|
145
|
-
e.preventDefault();
|
|
146
|
-
e.stopPropagation();
|
|
147
|
-
dragCounter.current--;
|
|
148
|
-
if (dragCounter.current === 0) {
|
|
149
|
-
setDragOver(false);
|
|
150
|
-
}
|
|
151
|
-
}, []);
|
|
152
|
-
|
|
153
|
-
const handleDragOver = useCallback((e: DragEvent) => {
|
|
154
|
-
e.preventDefault();
|
|
155
|
-
e.stopPropagation();
|
|
156
|
-
e.dataTransfer.dropEffect = "copy";
|
|
157
|
-
}, []);
|
|
158
|
-
|
|
159
|
-
const handleDrop = useCallback((e: DragEvent) => {
|
|
160
|
-
e.preventDefault();
|
|
161
|
-
e.stopPropagation();
|
|
162
|
-
dragCounter.current = 0;
|
|
163
|
-
setDragOver(false);
|
|
164
|
-
|
|
165
|
-
const files = e.dataTransfer?.files;
|
|
166
|
-
if (files?.length && onAttach) {
|
|
167
|
-
onAttach(files);
|
|
168
|
-
}
|
|
169
|
-
}, [onAttach]);
|
|
170
|
-
|
|
171
|
-
const fileChips = pendingFiles.filter((f) => f.type === "file" || !f.type);
|
|
172
|
-
const folderChips = pendingFiles.filter((f) => f.type === "folder");
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div
|
|
176
|
-
className={cn("relative", className)}
|
|
177
|
-
onDragEnter={onAttach ? handleDragEnter : undefined}
|
|
178
|
-
onDragLeave={onAttach ? handleDragLeave : undefined}
|
|
179
|
-
onDragOver={onAttach ? handleDragOver : undefined}
|
|
180
|
-
onDrop={onAttach ? handleDrop : undefined}
|
|
181
|
-
>
|
|
182
|
-
{/* Drop zone overlay */}
|
|
183
|
-
{dragOver && (
|
|
184
|
-
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-[var(--radius-xl)] border-2 border-dashed border-border bg-card pointer-events-none">
|
|
185
|
-
<div className="text-center">
|
|
186
|
-
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-[var(--accent-surface-soft)]">
|
|
187
|
-
<Upload className="h-6 w-6 text-primary" />
|
|
188
|
-
</div>
|
|
189
|
-
<p className="text-sm font-semibold text-foreground">{dropTitle}</p>
|
|
190
|
-
<p className="mt-1 text-xs text-muted-foreground">{dropDescription}</p>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
|
|
195
|
-
{/* Pending file chips */}
|
|
196
|
-
{pendingFiles.length > 0 && (
|
|
197
|
-
<div className="mb-3 flex flex-wrap gap-2">
|
|
198
|
-
{folderChips.map((f) => (
|
|
199
|
-
<span
|
|
200
|
-
key={f.id}
|
|
201
|
-
className={cn(
|
|
202
|
-
"inline-flex items-center gap-1.5 rounded-[var(--radius-full)] border px-3 py-1.5 text-xs",
|
|
203
|
-
"border-border bg-muted/50",
|
|
204
|
-
f.status === "error" && "border-[var(--code-error)]/30 text-[var(--code-error)]",
|
|
205
|
-
f.status !== "error" && "text-foreground",
|
|
206
|
-
)}
|
|
207
|
-
>
|
|
208
|
-
<FolderUp className="h-3 w-3 shrink-0" />
|
|
209
|
-
<span className="truncate max-w-[150px]">{f.name}</span>
|
|
210
|
-
{f.fileCount !== undefined && (
|
|
211
|
-
<span className="text-muted-foreground">({f.fileCount})</span>
|
|
212
|
-
)}
|
|
213
|
-
{f.status === "uploading" && (
|
|
214
|
-
<span className="w-3 h-3 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
|
215
|
-
)}
|
|
216
|
-
{onRemoveFile && (
|
|
217
|
-
<button
|
|
218
|
-
type="button"
|
|
219
|
-
aria-label={`Remove ${f.name}`}
|
|
220
|
-
onClick={() => onRemoveFile(f.id)}
|
|
221
|
-
className="rounded p-0.5 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
|
222
|
-
>
|
|
223
|
-
<X className="h-3 w-3" />
|
|
224
|
-
</button>
|
|
225
|
-
)}
|
|
226
|
-
</span>
|
|
227
|
-
))}
|
|
228
|
-
{fileChips.map((f) => (
|
|
229
|
-
<span
|
|
230
|
-
key={f.id}
|
|
231
|
-
className={cn(
|
|
232
|
-
"inline-flex items-center gap-1.5 rounded-[var(--radius-full)] border px-3 py-1.5 text-xs",
|
|
233
|
-
"border-border bg-muted/50",
|
|
234
|
-
f.status === "error" && "border-[var(--code-error)]/30 text-[var(--code-error)]",
|
|
235
|
-
f.status !== "error" && "text-foreground",
|
|
236
|
-
)}
|
|
237
|
-
>
|
|
238
|
-
<Paperclip className="h-3 w-3 shrink-0" />
|
|
239
|
-
<span className="truncate max-w-[150px]">{f.name}</span>
|
|
240
|
-
{f.status === "uploading" && (
|
|
241
|
-
<span className="w-3 h-3 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
|
242
|
-
)}
|
|
243
|
-
{onRemoveFile && (
|
|
244
|
-
<button
|
|
245
|
-
type="button"
|
|
246
|
-
aria-label={`Remove ${f.name}`}
|
|
247
|
-
onClick={() => onRemoveFile(f.id)}
|
|
248
|
-
className="rounded p-0.5 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
|
249
|
-
>
|
|
250
|
-
<X className="h-3 w-3" />
|
|
251
|
-
</button>
|
|
252
|
-
)}
|
|
253
|
-
</span>
|
|
254
|
-
))}
|
|
255
|
-
</div>
|
|
256
|
-
)}
|
|
257
|
-
|
|
258
|
-
{/* Input row */}
|
|
259
|
-
<div className="rounded-[24px] border border-[var(--chat-input-border,var(--border-default))] [background:var(--chat-input-bg,var(--bg-card))] shadow-[var(--chat-input-shadow,0_1px_2px_rgba(15,23,42,0.05))] transition-all focus-within:border-[var(--chat-input-focus-border,var(--border-accent))] focus-within:shadow-[var(--chat-input-focus-shadow,0_10px_30px_rgba(15,23,42,0.08))]">
|
|
260
|
-
<div className="rounded-[24px] px-4 py-[var(--chat-input-py)]">
|
|
261
|
-
{(inputLabel !== null || idleStatus !== null || streamingStatus !== null) && (
|
|
262
|
-
<div className="mb-1.5 flex items-center justify-between gap-3 px-1">
|
|
263
|
-
{inputLabel !== null && (
|
|
264
|
-
<div className="text-[var(--chat-label-size,11px)] font-[var(--chat-label-weight,600)] uppercase tracking-[var(--chat-label-tracking,0.16em)] text-[var(--text-muted)]">
|
|
265
|
-
{inputLabel}
|
|
266
|
-
</div>
|
|
267
|
-
)}
|
|
268
|
-
{(idleStatus !== null || streamingStatus !== null) && (
|
|
269
|
-
<div className="text-[var(--chat-label-size,11px)] text-[var(--text-muted)]">
|
|
270
|
-
{isStreaming ? (streamingStatus ?? "") : (idleStatus ?? "")}
|
|
271
|
-
</div>
|
|
272
|
-
)}
|
|
273
|
-
</div>
|
|
274
|
-
)}
|
|
275
|
-
<div className="flex items-end gap-2.5">
|
|
276
|
-
{/* Attach buttons */}
|
|
277
|
-
{onAttach && (
|
|
278
|
-
<>
|
|
279
|
-
<button
|
|
280
|
-
type="button"
|
|
281
|
-
onClick={handleAttachClick}
|
|
282
|
-
disabled={isStreaming}
|
|
283
|
-
aria-label="Attach files"
|
|
284
|
-
title="Attach files"
|
|
285
|
-
className="mb-0.5 shrink-0 rounded-[var(--radius-md)] border border-transparent p-2 text-muted-foreground transition-colors hover:border-border hover:bg-accent hover:text-foreground disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
|
286
|
-
>
|
|
287
|
-
<Paperclip className="h-4 w-4" />
|
|
288
|
-
</button>
|
|
289
|
-
<input
|
|
290
|
-
ref={fileInputRef}
|
|
291
|
-
type="file"
|
|
292
|
-
multiple
|
|
293
|
-
className="hidden"
|
|
294
|
-
onChange={handleFileChange}
|
|
295
|
-
accept={accept ?? ".pdf,.csv,.xlsx,.xls,.jpg,.jpeg,.png,.gif,.txt,.json,.yaml,.yml"}
|
|
296
|
-
/>
|
|
297
|
-
</>
|
|
298
|
-
)}
|
|
299
|
-
{(onAttachFolder ?? onAttach) && (
|
|
300
|
-
<>
|
|
301
|
-
<button
|
|
302
|
-
type="button"
|
|
303
|
-
onClick={handleFolderClick}
|
|
304
|
-
disabled={isStreaming}
|
|
305
|
-
aria-label="Attach folder"
|
|
306
|
-
title="Attach folder"
|
|
307
|
-
className="mb-0.5 shrink-0 rounded-[var(--radius-md)] border border-transparent p-2 text-muted-foreground transition-colors hover:border-border hover:bg-accent hover:text-foreground disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
|
308
|
-
>
|
|
309
|
-
<FolderUp className="h-4 w-4" />
|
|
310
|
-
</button>
|
|
311
|
-
<input
|
|
312
|
-
ref={folderInputRef}
|
|
313
|
-
type="file"
|
|
314
|
-
multiple
|
|
315
|
-
className="hidden"
|
|
316
|
-
onChange={handleFolderChange}
|
|
317
|
-
// @ts-ignore webkitdirectory is non-standard but widely supported
|
|
318
|
-
webkitdirectory=""
|
|
319
|
-
/>
|
|
320
|
-
</>
|
|
321
|
-
)}
|
|
322
|
-
|
|
323
|
-
{/* Textarea */}
|
|
324
|
-
<textarea
|
|
325
|
-
ref={textareaRef}
|
|
326
|
-
value={value}
|
|
327
|
-
onChange={handleChange}
|
|
328
|
-
onKeyDown={handleKeyDown}
|
|
329
|
-
placeholder={placeholder}
|
|
330
|
-
disabled={isStreaming || disabled}
|
|
331
|
-
rows={1}
|
|
332
|
-
aria-label="Message input"
|
|
333
|
-
className="min-h-[42px] max-h-[160px] flex-1 resize-none bg-transparent py-2 text-[15px] leading-6 text-foreground placeholder:text-muted-foreground disabled:opacity-50 focus-visible:outline-none"
|
|
334
|
-
/>
|
|
335
|
-
|
|
336
|
-
{/* Send / Cancel */}
|
|
337
|
-
{isStreaming ? (
|
|
338
|
-
<button
|
|
339
|
-
type="button"
|
|
340
|
-
onClick={onCancel}
|
|
341
|
-
aria-label="Stop response"
|
|
342
|
-
className="mb-0.5 shrink-0 rounded-[var(--radius-lg)] border border-[var(--code-error)]/20 bg-[var(--code-error)]/14 p-2.5 text-[var(--code-error)] transition-colors hover:bg-[var(--code-error)]/24 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--code-error)]/50"
|
|
343
|
-
>
|
|
344
|
-
<Square className="h-4 w-4" />
|
|
345
|
-
</button>
|
|
346
|
-
) : (
|
|
347
|
-
<button
|
|
348
|
-
type="button"
|
|
349
|
-
onClick={handleSend}
|
|
350
|
-
disabled={!value.trim() || disabled}
|
|
351
|
-
aria-label="Send message"
|
|
352
|
-
className="mb-0.5 inline-flex shrink-0 items-center gap-1.5 rounded-full border border-[var(--chat-send-border,var(--border-accent))] [background:var(--chat-send-bg,var(--brand-primary))] px-3.5 py-2.5 text-sm font-medium text-[var(--chat-send-color,white)] shadow-[var(--chat-send-shadow,0_6px_16px_rgba(15,23,42,0.12))] transition-all hover:translate-y-[-1px] hover:[background:var(--chat-send-hover-bg,var(--brand-strong))] disabled:opacity-30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-send-ring,var(--border-accent))]"
|
|
353
|
-
>
|
|
354
|
-
<Send className="h-4 w-4" />
|
|
355
|
-
<span>Send</span>
|
|
356
|
-
</button>
|
|
357
|
-
)}
|
|
358
|
-
</div>
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
|
|
362
|
-
{/* Footer: model selector + shortcuts */}
|
|
363
|
-
{(modelLabel || !hideShortcutHint) && (
|
|
364
|
-
<div className="mt-2 flex items-center justify-between px-1">
|
|
365
|
-
<div className="flex items-center gap-2">
|
|
366
|
-
{modelLabel && (
|
|
367
|
-
<button
|
|
368
|
-
type="button"
|
|
369
|
-
onClick={onModelClick}
|
|
370
|
-
aria-label={`Select model, current model ${modelLabel}`}
|
|
371
|
-
className="inline-flex items-center gap-1.5 rounded-[var(--radius-full)] border border-border bg-[linear-gradient(180deg,rgba(255,255,255,0.04),transparent)] px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:border-primary/20 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
|
372
|
-
>
|
|
373
|
-
<span className="w-1.5 h-1.5 rounded-full bg-[var(--code-success)]" />
|
|
374
|
-
{modelLabel}
|
|
375
|
-
</button>
|
|
376
|
-
)}
|
|
377
|
-
</div>
|
|
378
|
-
{!hideShortcutHint && (
|
|
379
|
-
<span className="text-xs text-muted-foreground">
|
|
380
|
-
<kbd className="px-1 py-0.5 bg-background rounded border border-border text-[10px]">Cmd</kbd>
|
|
381
|
-
<kbd className="px-1 py-0.5 bg-background rounded border border-border text-[10px] ml-0.5">L</kbd>
|
|
382
|
-
<span className="ml-1">to focus</span>
|
|
383
|
-
</span>
|
|
384
|
-
)}
|
|
385
|
-
</div>
|
|
386
|
-
)}
|
|
387
|
-
</div>
|
|
388
|
-
);
|
|
389
|
-
}
|