Skip to content

Commit 912a019

Browse files
committed
feat(web): add visible paste dialog for mobile terminal
- Replace hidden textarea with visible modal dialog - Show clear paste dialog when Clipboard API unavailable - Add textarea input with placeholder text for manual paste - Provide Cancel and Send buttons for user control - Add comprehensive CSS styling for paste dialog overlay - Update translations with dialog title and placeholder (en/zh) - Remove confusing "paste in text area" toast notification - Improve user experience with explicit visual feedback
1 parent fb0e7d5 commit 912a019

4 files changed

Lines changed: 150 additions & 36 deletions

File tree

packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,6 +2168,7 @@ export function XtermHost({
21682168

21692169
const fileInputRef = useRef<HTMLInputElement | null>(null);
21702170

2171+
const [showPasteDialog, setShowPasteDialog] = useState(false);
21712172
const pasteTextareaRef = useRef<HTMLTextAreaElement | null>(null);
21722173

21732174
const handleMobilePaste = useCallback(async () => {
@@ -2177,32 +2178,36 @@ export function XtermHost({
21772178
await handleClipboardPaste();
21782179
return;
21792180
} catch (error) {
2180-
console.debug("Clipboard API failed, trying fallback:", error);
2181+
console.debug("Clipboard API failed, showing paste dialog:", error);
21812182
}
21822183
}
21832184

2184-
// Fallback: use hidden textarea for manual paste
2185+
// Fallback: show paste dialog
2186+
setShowPasteDialog(true);
2187+
}, [handleClipboardPaste]);
2188+
2189+
const handlePasteDialogSubmit = useCallback(async () => {
21852190
const textarea = pasteTextareaRef.current;
21862191
if (!textarea) {
2187-
pushToast({
2188-
kind: "error",
2189-
title: t("terminal.mobile_paste_failed_title"),
2190-
body: t("terminal.mobile_paste_failed_body"),
2191-
});
21922192
return;
21932193
}
21942194

2195-
textarea.value = "";
2196-
textarea.focus();
2197-
textarea.select();
2195+
const text = textarea.value;
2196+
if (text) {
2197+
await sendTextToTerminal(text);
2198+
textarea.value = "";
2199+
}
21982200

2199-
pushToast({
2200-
kind: "info",
2201-
title: t("terminal.mobile_paste_manual_title"),
2202-
body: t("terminal.mobile_paste_manual_body"),
2203-
duration: 3_000,
2204-
});
2205-
}, [handleClipboardPaste, pushToast, t]);
2201+
setShowPasteDialog(false);
2202+
}, [sendTextToTerminal]);
2203+
2204+
const handlePasteDialogCancel = useCallback(() => {
2205+
const textarea = pasteTextareaRef.current;
2206+
if (textarea) {
2207+
textarea.value = "";
2208+
}
2209+
setShowPasteDialog(false);
2210+
}, []);
22062211

22072212
const handleMobileUpload = useCallback(() => {
22082213
fileInputRef.current?.click();
@@ -2277,17 +2282,6 @@ export function XtermHost({
22772282
void handleFileInputChange(event);
22782283
}}
22792284
/>
2280-
<textarea
2281-
ref={pasteTextareaRef}
2282-
hidden
2283-
onChange={async (event) => {
2284-
const text = event.currentTarget.value;
2285-
if (text) {
2286-
await sendTextToTerminal(text);
2287-
event.currentTarget.value = "";
2288-
}
2289-
}}
2290-
/>
22912285
<div
22922286
ref={containerRef}
22932287
className="xterm-host"
@@ -2327,6 +2321,44 @@ export function XtermHost({
23272321
Uploading…
23282322
</div>
23292323
) : null}
2324+
{showPasteDialog ? (
2325+
<div
2326+
className="paste-dialog-overlay"
2327+
role="dialog"
2328+
aria-modal="true"
2329+
aria-labelledby="paste-dialog-title"
2330+
>
2331+
<div className="paste-dialog">
2332+
<h3 id="paste-dialog-title" className="paste-dialog__title">
2333+
{t("terminal.paste_dialog_title")}
2334+
</h3>
2335+
<textarea
2336+
ref={pasteTextareaRef}
2337+
className="paste-dialog__textarea"
2338+
placeholder={t("terminal.paste_dialog_placeholder")}
2339+
autoFocus
2340+
/>
2341+
<div className="paste-dialog__actions">
2342+
<button
2343+
type="button"
2344+
className="paste-dialog__button paste-dialog__button--secondary"
2345+
onClick={handlePasteDialogCancel}
2346+
>
2347+
{t("common.cancel")}
2348+
</button>
2349+
<button
2350+
type="button"
2351+
className="paste-dialog__button paste-dialog__button--primary"
2352+
onClick={() => {
2353+
void handlePasteDialogSubmit();
2354+
}}
2355+
>
2356+
{t("terminal.paste_dialog_submit")}
2357+
</button>
2358+
</div>
2359+
</div>
2360+
</div>
2361+
) : null}
23302362
{viewport !== "mobile" && hydrationState.kind === "queued" ? (
23312363
<XtermPlaceholder state="queued" queuePosition={hydrationState.queuePosition} />
23322364
) : null}

packages/web/src/locales/en.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,9 @@
262262
"copied_current_line": "Copied current line",
263263
"mobile_copy_current_line_failed_title": "Current line copy failed",
264264
"mobile_copy_current_line_failed_body": "Try long-pressing the current line again",
265-
"mobile_paste_failed_title": "Paste failed",
266-
"mobile_paste_failed_body": "Could not paste from clipboard",
267-
"mobile_paste_manual_title": "Paste",
268-
"mobile_paste_manual_body": "Please paste your text in the text area",
265+
"paste_dialog_title": "Paste Text",
266+
"paste_dialog_placeholder": "Paste your text here...",
267+
"paste_dialog_submit": "Send",
269268
"hide": "Hide Terminal",
270269
"show": "Show Terminal",
271270
"selector": {

packages/web/src/locales/zh.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,9 @@
262262
"copied_current_line": "已复制当前行",
263263
"mobile_copy_current_line_failed_title": "当前行复制失败",
264264
"mobile_copy_current_line_failed_body": "请重试长按当前行",
265-
"mobile_paste_failed_title": "粘贴失败",
266-
"mobile_paste_failed_body": "无法从剪贴板粘贴",
267-
"mobile_paste_manual_title": "粘贴",
268-
"mobile_paste_manual_body": "请在文本框中粘贴您的文本",
265+
"paste_dialog_title": "粘贴文本",
266+
"paste_dialog_placeholder": "在此粘贴您的文本...",
267+
"paste_dialog_submit": "发送",
269268
"hide": "隐藏终端",
270269
"show": "显示终端",
271270
"selector": {

packages/web/src/styles/components.css

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11154,3 +11154,87 @@ textarea.input {
1115411154
.mobile-sheet__footer .git-panel-status-strip {
1115511155
padding-top: 0;
1115611156
}
11157+
11158+
/* ========== Paste Dialog ========== */
11159+
.paste-dialog-overlay {
11160+
position: absolute;
11161+
inset: 0;
11162+
display: flex;
11163+
align-items: center;
11164+
justify-content: center;
11165+
background: rgba(0, 0, 0, 0.6);
11166+
z-index: 10;
11167+
}
11168+
11169+
.paste-dialog {
11170+
display: flex;
11171+
flex-direction: column;
11172+
gap: var(--sp-3);
11173+
width: 90%;
11174+
max-width: 24rem;
11175+
padding: var(--sp-4);
11176+
background: var(--bg-surface);
11177+
border: 1px solid var(--border);
11178+
border-radius: var(--radius-xl);
11179+
box-shadow: var(--shadow-xl);
11180+
}
11181+
11182+
.paste-dialog__title {
11183+
margin: 0;
11184+
font-size: var(--text-base);
11185+
font-weight: var(--font-semibold);
11186+
color: var(--text-primary);
11187+
}
11188+
11189+
.paste-dialog__textarea {
11190+
min-height: 6rem;
11191+
padding: var(--sp-3);
11192+
background: var(--bg-page);
11193+
border: 1px solid var(--border);
11194+
border-radius: var(--radius-md);
11195+
color: var(--text-primary);
11196+
font-family: var(--font-mono);
11197+
font-size: var(--text-sm);
11198+
resize: vertical;
11199+
}
11200+
11201+
.paste-dialog__textarea:focus {
11202+
outline: none;
11203+
border-color: var(--accent-blue);
11204+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-blue) 20%, transparent);
11205+
}
11206+
11207+
.paste-dialog__actions {
11208+
display: flex;
11209+
gap: var(--sp-2);
11210+
justify-content: flex-end;
11211+
}
11212+
11213+
.paste-dialog__button {
11214+
padding: var(--sp-2) var(--sp-4);
11215+
border: 1px solid var(--border);
11216+
border-radius: var(--radius-md);
11217+
font-size: var(--text-sm);
11218+
font-weight: var(--font-medium);
11219+
cursor: pointer;
11220+
transition: all var(--duration-fast) var(--ease-out);
11221+
}
11222+
11223+
.paste-dialog__button--secondary {
11224+
background: var(--bg-page);
11225+
color: var(--text-secondary);
11226+
}
11227+
11228+
.paste-dialog__button--secondary:hover {
11229+
background: color-mix(in srgb, var(--bg-surface) 50%, var(--bg-page) 50%);
11230+
}
11231+
11232+
.paste-dialog__button--primary {
11233+
background: var(--accent-blue);
11234+
color: white;
11235+
border-color: var(--accent-blue);
11236+
}
11237+
11238+
.paste-dialog__button--primary:hover {
11239+
background: color-mix(in srgb, var(--accent-blue) 85%, white);
11240+
}

0 commit comments

Comments
 (0)