fix(web): replace SketchEditor text prompt with modal

Replace the SketchEditor text tool's window.prompt flow with an in-app modal so it works in Electron desktop.
This commit is contained in:
Sid 2026-05-07 14:00:25 +08:00 committed by GitHub
parent 0b2b456694
commit 01c22a87bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 91 additions and 7 deletions

View file

@ -73,6 +73,13 @@ export function SketchEditor({
const [size, setSize] = useState(2);
const drawingRef = useRef<SketchItem | null>(null);
const [, force] = useState(0);
// Text-tool modal. Replaces window.prompt() because Electron 28+
// disables that API by default and silently returns null, making
// the text tool a no-op in the desktop app. Same root cause as
// issue #723 (FileViewer's Save-as-template flow).
const [textModalOpen, setTextModalOpen] = useState(false);
const [textModalValue, setTextModalValue] = useState('');
const textAnchorRef = useRef<{ x: number; y: number } | null>(null);
// Resize canvas to its container while keeping a high DPR for crisp lines.
useEffect(() => {
@ -126,13 +133,11 @@ export function SketchEditor({
const pos = pointerPos(e);
if (tool === 'text') {
const text = window.prompt(t('sketch.textPrompt'));
if (text) {
onItemsChange([
...items,
{ kind: 'text', x: pos.x, y: pos.y, text, color, size: 16 + size * 4 },
]);
}
// Stash the click position and open the modal. The actual TextItem is
// appended in submitTextModal, once the user confirms.
textAnchorRef.current = pos;
setTextModalValue('');
setTextModalOpen(true);
return;
}
@ -189,6 +194,28 @@ export function SketchEditor({
onItemsChange([]);
}
function submitTextModal() {
const text = textModalValue.trim();
const anchor = textAnchorRef.current;
if (!text || !anchor) {
cancelTextModal();
return;
}
onItemsChange([
...items,
{ kind: 'text', x: anchor.x, y: anchor.y, text, color, size: 16 + size * 4 },
]);
setTextModalOpen(false);
setTextModalValue('');
textAnchorRef.current = null;
}
function cancelTextModal() {
setTextModalOpen(false);
setTextModalValue('');
textAnchorRef.current = null;
}
return (
<div className="sketch-editor">
<div className="sketch-toolbar">
@ -250,6 +277,46 @@ export function SketchEditor({
style={{ touchAction: 'none' }}
/>
</div>
{textModalOpen ? (
<div className="modal-backdrop" role="presentation">
<div className="modal" role="dialog" aria-modal="true">
<div className="modal-head">
<h2>{t('sketch.textModalTitle')}</h2>
</div>
<label>
<span>{t('sketch.textPrompt')}</span>
<input
type="text"
value={textModalValue}
autoFocus
onChange={(e) => setTextModalValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && textModalValue.trim()) {
e.preventDefault();
submitTextModal();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelTextModal();
}
}}
/>
</label>
<div className="modal-foot">
<button type="button" className="ghost" onClick={cancelTextModal}>
{t('common.cancel')}
</button>
<button
type="button"
className="primary"
disabled={!textModalValue.trim()}
onClick={submitTextModal}
>
{t('common.save')}
</button>
</div>
</div>
</div>
) : null}
</div>
);
}

View file

@ -818,6 +818,7 @@ export const ar: Dict = {
'sketch.clear': 'مسح',
'sketch.close': 'إغلاق',
'sketch.textPrompt': 'النص:',
'sketch.textModalTitle': 'إضافة نص',
'pet.title': 'الحيوانات الأليفة',
'pet.subtitle': 'تبنَّ رفيقاً صغيراً يطفو فوق مساحة عملك.',

View file

@ -774,6 +774,7 @@ export const de: Dict = {
'sketch.clear': 'Leeren',
'sketch.close': 'Schließen',
'sketch.textPrompt': 'Text:',
'sketch.textModalTitle': 'Text hinzufügen',
'pet.title': 'Haustiere',
'pet.tabBuiltIn': 'Vorgegeben',

View file

@ -851,6 +851,7 @@ export const en: Dict = {
'sketch.clear': 'Clear',
'sketch.close': 'Close',
'sketch.textPrompt': 'Text:',
'sketch.textModalTitle': 'Add text',
'pet.title': 'Pets',
'pet.subtitle': 'Adopt a tiny companion that floats over your workspace.',

View file

@ -775,6 +775,7 @@ export const esES: Dict = {
'sketch.clear': 'Limpiar',
'sketch.close': 'Cerrar',
'sketch.textPrompt': 'Texto:',
'sketch.textModalTitle': 'Añadir texto',
'pet.title': 'Mascotas',
'pet.tabBuiltIn': 'Integradas',

View file

@ -852,6 +852,7 @@ export const fa: Dict = {
'sketch.clear': 'پاک کردن',
'sketch.close': 'بستن',
'sketch.textPrompt': 'متن:',
'sketch.textModalTitle': 'افزودن متن',
'pet.title': 'حیوان خانگی',
'pet.tabBuiltIn': 'پیش‌فرض',

View file

@ -818,6 +818,7 @@ export const fr: Dict = {
'sketch.clear': 'Effacer',
'sketch.close': 'Fermer',
'sketch.textPrompt': 'Texte :',
'sketch.textModalTitle': 'Ajouter du texte',
'pet.title': 'Compagnons',
'pet.subtitle': 'Adoptez un petit compagnon qui flotte au-dessus de votre espace de travail.',

View file

@ -820,6 +820,7 @@ export const hu: Dict = {
'sketch.clear': 'Törlés',
'sketch.close': 'Bezárás',
'sketch.textPrompt': 'Szöveg:',
'sketch.textModalTitle': 'Szöveg hozzáadása',
'pet.title': 'Háziállatok',
'pet.subtitle': 'Fogadj be egy apró társat, aki a munkaterületed felett lebeg.',

View file

@ -773,6 +773,7 @@ export const ja: Dict = {
'sketch.clear': 'クリア',
'sketch.close': '閉じる',
'sketch.textPrompt': 'テキスト:',
'sketch.textModalTitle': 'テキストを追加',
'pet.title': 'ペット',
'pet.tabBuiltIn': '組み込み',

View file

@ -820,6 +820,7 @@ export const ko: Dict = {
'sketch.clear': '모두 지우기',
'sketch.close': '닫기',
'sketch.textPrompt': '텍스트:',
'sketch.textModalTitle': '텍스트 추가',
'pet.title': '펫',
'pet.tabBuiltIn': '내장',

View file

@ -820,6 +820,7 @@ export const pl: Dict = {
'sketch.clear': 'Wyczyść',
'sketch.close': 'Zamknij',
'sketch.textPrompt': 'Tekst:',
'sketch.textModalTitle': 'Dodaj tekst',
'pet.title': 'Pupile',
'pet.tabBuiltIn': 'Wbudowane',

View file

@ -850,6 +850,7 @@ export const ptBR: Dict = {
'sketch.clear': 'Limpar',
'sketch.close': 'Fechar',
'sketch.textPrompt': 'Texto:',
'sketch.textModalTitle': 'Adicionar texto',
'pet.title': 'Bichinhos',
'pet.tabBuiltIn': 'Inclusos',

View file

@ -850,6 +850,7 @@ export const ru: Dict = {
'sketch.clear': 'Очистить',
'sketch.close': 'Закрыть',
'sketch.textPrompt': 'Текст:',
'sketch.textModalTitle': 'Добавить текст',
'pet.title': 'Питомцы',
'pet.tabBuiltIn': 'Встроенные',

View file

@ -812,6 +812,7 @@ export const tr: Dict = {
'sketch.clear': 'Temizle',
'sketch.close': 'Kapat',
'sketch.textPrompt': 'Metin:',
'sketch.textModalTitle': 'Metin ekle',
'pet.title': 'Evcil Dostlar',
'pet.tabBuiltIn': 'Yerleşik',

View file

@ -851,6 +851,7 @@ export const uk: Dict = {
'sketch.clear': 'Очистити',
'sketch.close': 'Закрити',
'sketch.textPrompt': 'Текст:',
'sketch.textModalTitle': 'Додати текст',
'pet.title': 'Маскоти',
'pet.subtitle': 'Встановити крихітного супутника, який пливе над вашим робочим простором.',

View file

@ -832,6 +832,7 @@ export const zhCN: Dict = {
'sketch.clear': '清空',
'sketch.close': '关闭',
'sketch.textPrompt': '请输入文本:',
'sketch.textModalTitle': '添加文本',
'pet.title': '宠物',
'pet.tabBuiltIn': '内置',

View file

@ -832,6 +832,7 @@ export const zhTW: Dict = {
'sketch.clear': '清除',
'sketch.close': '關閉',
'sketch.textPrompt': '請輸入文字:',
'sketch.textModalTitle': '新增文字',
'pet.title': '寵物',
'pet.tabBuiltIn': '內建',

View file

@ -1024,4 +1024,5 @@ export interface Dict {
'sketch.clear': string;
'sketch.close': string;
'sketch.textPrompt': string;
'sketch.textModalTitle': string;
}