feat: refine codex live chat context flows

This commit is contained in:
2026-05-08 21:15:51 +09:00
parent 82c0d8a197
commit 442879313f
92 changed files with 14815 additions and 7314 deletions

117
src/utils/clipboard.ts Normal file
View File

@@ -0,0 +1,117 @@
type SelectionSnapshot = {
activeElement: Element | null;
ranges: Range[];
};
function captureSelectionSnapshot(): SelectionSnapshot | null {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return null;
}
const selection = window.getSelection();
const ranges: Range[] = [];
if (selection) {
for (let index = 0; index < selection.rangeCount; index += 1) {
ranges.push(selection.getRangeAt(index).cloneRange());
}
}
return {
activeElement: document.activeElement,
ranges,
};
}
function restoreSelectionSnapshot(snapshot: SelectionSnapshot | null) {
if (!snapshot || typeof window === 'undefined') {
return;
}
const selection = window.getSelection();
selection?.removeAllRanges();
snapshot.ranges.forEach((range) => selection?.addRange(range));
const target = snapshot.activeElement;
if (target instanceof HTMLElement || target instanceof SVGElement) {
try {
target.focus({ preventScroll: true });
} catch {
target.focus();
}
}
}
function copyUsingTextareaFallback(text: string) {
if (
typeof window === 'undefined' ||
typeof document === 'undefined' ||
typeof document.execCommand !== 'function' ||
!document.body
) {
return false;
}
const snapshot = captureSelectionSnapshot();
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.tabIndex = -1;
textarea.readOnly = true;
textarea.setAttribute('readonly', '');
textarea.setAttribute('aria-hidden', 'true');
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.width = '1px';
textarea.style.height = '1px';
textarea.style.padding = '0';
textarea.style.border = '0';
textarea.style.outline = '0';
textarea.style.boxShadow = 'none';
textarea.style.background = 'transparent';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
textarea.style.fontSize = '16px';
textarea.style.whiteSpace = 'pre';
textarea.style.userSelect = 'text';
textarea.style.webkitUserSelect = 'text';
document.body.appendChild(textarea);
try {
try {
textarea.focus({ preventScroll: true });
} catch {
textarea.focus();
}
textarea.select();
textarea.selectionStart = 0;
textarea.selectionEnd = text.length;
textarea.setSelectionRange(0, text.length);
return document.execCommand('copy');
} catch {
return false;
} finally {
textarea.remove();
restoreSelectionSnapshot(snapshot);
}
}
export async function copyTextToClipboard(text: string): Promise<void> {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return;
} catch {
// Fall back when the browser exposes the API but rejects the write.
}
}
if (copyUsingTextareaFallback(text)) {
return;
}
throw new Error('브라우저가 클립보드 복사를 차단했습니다.');
}