diff --git a/apps/web/src/components/EntryShell.tsx b/apps/web/src/components/EntryShell.tsx index eda9da33e9..a34bef1e94 100644 --- a/apps/web/src/components/EntryShell.tsx +++ b/apps/web/src/components/EntryShell.tsx @@ -132,6 +132,7 @@ interface Props { designSystems: DesignSystemSummary[]; projects: Project[]; templates: ProjectTemplate[]; + onDeleteTemplate?: (id: string) => Promise; promptTemplates: PromptTemplateSummary[]; defaultDesignSystemId: string | null; connectors: ConnectorDetail[]; @@ -215,6 +216,7 @@ export function EntryShell({ designSystems, projects, templates, + onDeleteTemplate, promptTemplates, defaultDesignSystemId, connectors, @@ -603,6 +605,7 @@ export function EntryShell({ designSystems={designSystems} defaultDesignSystemId={defaultDesignSystemId} templates={templates} + {...(onDeleteTemplate ? { onDeleteTemplate } : {})} promptTemplates={promptTemplates} connectors={connectors} connectorsLoading={connectorsLoading} diff --git a/apps/web/src/components/EntryView.tsx b/apps/web/src/components/EntryView.tsx index e744318150..013b8cc234 100644 --- a/apps/web/src/components/EntryView.tsx +++ b/apps/web/src/components/EntryView.tsx @@ -330,6 +330,7 @@ export function EntryView({ designSystems={designSystems} projects={projects} templates={templates} + onDeleteTemplate={onDeleteTemplate} promptTemplates={promptTemplates} defaultDesignSystemId={defaultDesignSystemId} connectors={connectors} diff --git a/apps/web/src/components/NewProjectModal.tsx b/apps/web/src/components/NewProjectModal.tsx index 6f5f4a4081..80a53ebfac 100644 --- a/apps/web/src/components/NewProjectModal.tsx +++ b/apps/web/src/components/NewProjectModal.tsx @@ -26,6 +26,7 @@ interface Props { designSystems: DesignSystemSummary[]; defaultDesignSystemId: string | null; templates: ProjectTemplate[]; + onDeleteTemplate?: (id: string) => Promise; promptTemplates: PromptTemplateSummary[]; mediaProviders?: Record; connectors?: ConnectorDetail[]; @@ -45,6 +46,7 @@ export function NewProjectModal({ designSystems, defaultDesignSystemId, templates, + onDeleteTemplate, promptTemplates, mediaProviders, connectors, @@ -115,6 +117,7 @@ export function NewProjectModal({ designSystems={designSystems} defaultDesignSystemId={defaultDesignSystemId} templates={templates} + {...(onDeleteTemplate ? { onDeleteTemplate } : {})} promptTemplates={promptTemplates} {...(mediaProviders ? { mediaProviders } : {})} {...(connectors ? { connectors } : {})} diff --git a/apps/web/src/components/NewProjectPanel.tsx b/apps/web/src/components/NewProjectPanel.tsx index 56583a7f50..9dae8f69e7 100644 --- a/apps/web/src/components/NewProjectPanel.tsx +++ b/apps/web/src/components/NewProjectPanel.tsx @@ -1300,9 +1300,57 @@ function TemplatePicker({ onDelete?: (id: string) => Promise; }) { const t = useT(); + const [confirmTarget, setConfirmTarget] = useState(null); + const [deleteError, setDeleteError] = useState(null); + const [deleting, setDeleting] = useState(false); + + function dismissConfirm() { + if (deleting) return; + setConfirmTarget(null); + setDeleteError(null); + } + + useEffect(() => { + if (!confirmTarget) return; + const onKey = (e: KeyboardEvent) => { + if (e.key !== 'Escape') return; + e.preventDefault(); + e.stopPropagation(); + if (deleting) return; + setConfirmTarget(null); + setDeleteError(null); + }; + document.addEventListener('keydown', onKey, true); + return () => document.removeEventListener('keydown', onKey, true); + }, [confirmTarget, deleting]); + + async function handleConfirmDelete() { + if (!confirmTarget || !onDelete) return; + setDeleting(true); + setDeleteError(null); + try { + const ok = await onDelete(confirmTarget.id); + if (!ok) { + setDeleteError(t('newproj.deleteTemplateError')); + return; + } + if (value === confirmTarget.id) onChange(null); + setConfirmTarget(null); + } catch { + setDeleteError(t('newproj.deleteTemplateError')); + } finally { + setDeleting(false); + } + } + return (
+ {deleteError ? ( +

+ {deleteError} +

+ ) : null} {templates.length === 0 ? (
@@ -1325,10 +1373,14 @@ function TemplatePicker({ key={tpl.id} active={value === tpl.id} onClick={() => onChange(tpl.id)} - onDelete={onDelete ? async () => { - const ok = await onDelete(tpl.id); - if (ok && value === tpl.id) onChange(null); - } : () => {}} + onDelete={ + onDelete + ? () => { + setDeleteError(null); + setConfirmTarget(tpl); + } + : () => {} + } name={tpl.name} description={tpl.description ?? fallbackDesc} /> @@ -1336,6 +1388,50 @@ function TemplatePicker({ })}
)} + {confirmTarget ? ( +
+
e.stopPropagation()} + onKeyDown={(e) => { + if (e.key !== 'Escape') return; + e.stopPropagation(); + dismissConfirm(); + }} + role="alertdialog" + aria-modal="true" + data-testid="template-delete-confirm-dialog" + > +

{t('newproj.deleteTemplateTitle')}

+

+ {t('newproj.deleteTemplateBody', { name: confirmTarget.name })} +

+
+ + +
+
+
+ ) : null}
); } @@ -1649,6 +1745,7 @@ function TemplateOption({ name: string; description: string; }) { + const t = useT(); return (
diff --git a/apps/web/src/i18n/locales/ar.ts b/apps/web/src/i18n/locales/ar.ts index 58b669abfe..ec48108a8a 100644 --- a/apps/web/src/i18n/locales/ar.ts +++ b/apps/web/src/i18n/locales/ar.ts @@ -473,6 +473,12 @@ export const ar: Dict = { 'newproj.savedTemplate': 'قالب محفوظ', 'newproj.fileSingular': 'ملف', 'newproj.filePlural': 'ملفات', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'إنشاء', 'newproj.createFromTemplate': 'إنشاء من قالب', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index d721e79558..84e6072d2b 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -370,6 +370,12 @@ export const de: Dict = { 'newproj.savedTemplate': 'Gespeichertes Template', 'newproj.fileSingular': 'Datei', 'newproj.filePlural': 'Dateien', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Erstellen', 'newproj.createFromTemplate': 'Aus Template erstellen', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index 0c192d56f2..2199a91da1 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -924,6 +924,12 @@ export const en: Dict = { 'newproj.savedTemplate': 'Saved template', 'newproj.fileSingular': 'file', 'newproj.filePlural': 'files', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Create', 'newproj.createLiveArtifact': 'Create live artifact', 'newproj.createFromTemplate': 'Create from template', diff --git a/apps/web/src/i18n/locales/es-ES.ts b/apps/web/src/i18n/locales/es-ES.ts index 31c7f6ff7d..5de58f105d 100644 --- a/apps/web/src/i18n/locales/es-ES.ts +++ b/apps/web/src/i18n/locales/es-ES.ts @@ -371,6 +371,12 @@ export const esES: Dict = { 'newproj.savedTemplate': 'Plantilla guardada', 'newproj.fileSingular': 'archivo', 'newproj.filePlural': 'archivos', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Crear', 'newproj.createFromTemplate': 'Crear desde plantilla', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/fa.ts b/apps/web/src/i18n/locales/fa.ts index 7c1e9566c2..48f26a1f55 100644 --- a/apps/web/src/i18n/locales/fa.ts +++ b/apps/web/src/i18n/locales/fa.ts @@ -485,6 +485,12 @@ export const fa: Dict = { 'newproj.savedTemplate': 'قالب ذخیره شده', 'newproj.fileSingular': 'فایل', 'newproj.filePlural': 'فایل', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'ایجاد', 'newproj.createLiveArtifact': 'ایجاد مصنوع زنده', 'newproj.createFromTemplate': 'ایجاد از قالب', diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index e448b57671..22ca304652 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -473,6 +473,12 @@ export const fr: Dict = { 'newproj.savedTemplate': 'Modèle enregistré', 'newproj.fileSingular': 'fichier', 'newproj.filePlural': 'fichiers', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Créer', 'newproj.createFromTemplate': 'Créer depuis le modèle', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index d5dd62fbe9..996f1b7775 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -473,6 +473,12 @@ export const hu: Dict = { 'newproj.savedTemplate': 'Mentett sablon', 'newproj.fileSingular': 'fájl', 'newproj.filePlural': 'fájl', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Létrehozás', 'newproj.createFromTemplate': 'Létrehozás sablonból', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/id.ts b/apps/web/src/i18n/locales/id.ts index baa135fb5c..02de7ac7a4 100644 --- a/apps/web/src/i18n/locales/id.ts +++ b/apps/web/src/i18n/locales/id.ts @@ -583,6 +583,12 @@ export const id: Dict = { 'newproj.savedTemplate': 'Templat tersimpan', 'newproj.fileSingular': 'berkas', 'newproj.filePlural': 'berkas', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Buat', 'newproj.createLiveArtifact': 'Buat live artifact', 'newproj.createFromTemplate': 'Buat dari templat', diff --git a/apps/web/src/i18n/locales/it.ts b/apps/web/src/i18n/locales/it.ts index 87cf4cc70f..cf9ae8a681 100644 --- a/apps/web/src/i18n/locales/it.ts +++ b/apps/web/src/i18n/locales/it.ts @@ -446,6 +446,12 @@ export const it: Dict = { 'newproj.savedTemplate': 'Modello salvato', 'newproj.fileSingular': 'file', 'newproj.filePlural': 'file', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Crea', 'newproj.createFromTemplate': 'Crea dal modello', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index 34403f1719..878dcbbdca 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -370,6 +370,12 @@ export const ja: Dict = { 'newproj.savedTemplate': '保存済みテンプレート', 'newproj.fileSingular': 'ファイル', 'newproj.filePlural': 'ファイル', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': '作成', 'newproj.createFromTemplate': 'テンプレートから作成', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index dbdb7eb325..11a36fba2d 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -473,6 +473,12 @@ export const ko: Dict = { 'newproj.savedTemplate': '저장된 템플릿', 'newproj.fileSingular': '파일', 'newproj.filePlural': '파일들', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': '생성', 'newproj.createFromTemplate': '템플릿으로 생성', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/pl.ts b/apps/web/src/i18n/locales/pl.ts index 569a451fbb..278449053f 100644 --- a/apps/web/src/i18n/locales/pl.ts +++ b/apps/web/src/i18n/locales/pl.ts @@ -473,6 +473,12 @@ export const pl: Dict = { 'newproj.savedTemplate': 'Zapisany szablon', 'newproj.fileSingular': 'plik', 'newproj.filePlural': 'pliki', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Utwórz', 'newproj.createFromTemplate': 'Utwórz z szablonu', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts index 5954fb3e0b..2eaa94f4fd 100644 --- a/apps/web/src/i18n/locales/pt-BR.ts +++ b/apps/web/src/i18n/locales/pt-BR.ts @@ -483,6 +483,12 @@ export const ptBR: Dict = { 'newproj.savedTemplate': 'Template salvo', 'newproj.fileSingular': 'arquivo', 'newproj.filePlural': 'arquivos', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Criar', 'newproj.createLiveArtifact': 'Criar artefato live', 'newproj.createFromTemplate': 'Criar a partir do template', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index 8bed3d4674..359accc396 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -483,6 +483,12 @@ export const ru: Dict = { 'newproj.savedTemplate': 'Сохраненный шаблон', 'newproj.fileSingular': 'файл', 'newproj.filePlural': 'файлов', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Создать', 'newproj.createLiveArtifact': 'Создать live-артефакт', 'newproj.createFromTemplate': 'Создать из шаблона', diff --git a/apps/web/src/i18n/locales/th.ts b/apps/web/src/i18n/locales/th.ts index 33de3fb8c0..881d422b13 100644 --- a/apps/web/src/i18n/locales/th.ts +++ b/apps/web/src/i18n/locales/th.ts @@ -442,6 +442,12 @@ export const th: Dict = { 'newproj.savedTemplate': 'เทมเพลตที่บันทึกแล้ว', 'newproj.fileSingular': 'ไฟล์', 'newproj.filePlural': 'ไฟล์', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'สร้าง', 'newproj.createLiveArtifact': 'สร้าง live artifact', 'newproj.createFromTemplate': 'สร้างจากเทมเพลต', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index c7c4e02496..c8b40e06c8 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -463,6 +463,12 @@ export const tr: Dict = { 'newproj.savedTemplate': 'Şablon kaydedildi', 'newproj.fileSingular': 'dosya', 'newproj.filePlural': 'dosyalar', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Oluştur', 'newproj.createFromTemplate': 'Şablondan oluştur', 'newproj.createDisabledTitle': diff --git a/apps/web/src/i18n/locales/uk.ts b/apps/web/src/i18n/locales/uk.ts index 31b1608df9..892c02065f 100644 --- a/apps/web/src/i18n/locales/uk.ts +++ b/apps/web/src/i18n/locales/uk.ts @@ -485,6 +485,12 @@ export const uk: Dict = { 'newproj.savedTemplate': 'Збережений шаблон', 'newproj.fileSingular': 'файл', 'newproj.filePlural': 'файли', + 'newproj.deleteTemplateTitle': 'Delete template?', + 'newproj.deleteTemplateBody': + 'This will permanently remove "{name}" from your saved templates.', + 'newproj.deleteTemplateConfirm': 'Delete', + 'newproj.deleteTemplateError': 'Could not delete template. Try again.', + 'newproj.deleteTemplateAria': 'Delete template {name}', 'newproj.create': 'Створити', 'newproj.createLiveArtifact': 'Створити live-артефакт', 'newproj.createFromTemplate': 'Створити з шаблону', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index f66f5a61ed..a0408ff861 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -922,6 +922,12 @@ export const zhCN: Dict = { 'newproj.savedTemplate': '已保存的模板', 'newproj.fileSingular': '个文件', 'newproj.filePlural': '个文件', + 'newproj.deleteTemplateTitle': '删除模板?', + 'newproj.deleteTemplateBody': + '这将永久从已保存的模板中移除“{name}”。', + 'newproj.deleteTemplateConfirm': '删除', + 'newproj.deleteTemplateError': '无法删除模板,请重试。', + 'newproj.deleteTemplateAria': '删除模板 {name}', 'newproj.create': '创建', 'newproj.createLiveArtifact': '创建实时制品', 'newproj.createFromTemplate': '基于模板创建', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index 2fe4c46d7c..4c548f3956 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -543,6 +543,12 @@ export const zhTW: Dict = { 'newproj.savedTemplate': '已儲存的範本', 'newproj.fileSingular': '個檔案', 'newproj.filePlural': '個檔案', + 'newproj.deleteTemplateTitle': '刪除範本?', + 'newproj.deleteTemplateBody': + '這將永久從已儲存的範本中移除「{name}」。', + 'newproj.deleteTemplateConfirm': '刪除', + 'newproj.deleteTemplateError': '無法刪除範本,請重試。', + 'newproj.deleteTemplateAria': '刪除範本 {name}', 'newproj.create': '建立', 'newproj.createLiveArtifact': '建立即時成品', 'newproj.createFromTemplate': '基於範本建立', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index dcf62dda31..9074e993e6 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -1166,6 +1166,11 @@ export interface Dict { 'newproj.savedTemplate': string; 'newproj.fileSingular': string; 'newproj.filePlural': string; + 'newproj.deleteTemplateTitle': string; + 'newproj.deleteTemplateBody': string; + 'newproj.deleteTemplateConfirm': string; + 'newproj.deleteTemplateError': string; + 'newproj.deleteTemplateAria': string; 'newproj.create': string; 'newproj.createLiveArtifact': string; 'newproj.createFromTemplate': string; diff --git a/apps/web/src/styles/home/new-project-modal.css b/apps/web/src/styles/home/new-project-modal.css index 68f8c60e83..e73e27783f 100644 --- a/apps/web/src/styles/home/new-project-modal.css +++ b/apps/web/src/styles/home/new-project-modal.css @@ -2,6 +2,10 @@ New project modal — wraps NewProjectPanel in a centered modal surface, triggered by the "+" button on the entry nav rail. ============================================================ */ +.template-delete-confirm-backdrop { + z-index: 940; +} + .new-project-modal-backdrop { position: fixed; inset: 0; diff --git a/apps/web/tests/components/NewProjectModal.test.tsx b/apps/web/tests/components/NewProjectModal.test.tsx index 1bf793cc0b..d9542b4ea4 100644 --- a/apps/web/tests/components/NewProjectModal.test.tsx +++ b/apps/web/tests/components/NewProjectModal.test.tsx @@ -1,10 +1,10 @@ // @vitest-environment jsdom -import { cleanup, render, screen } from '@testing-library/react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { NewProjectModal } from '../../src/components/NewProjectModal'; -import type { DesignSystemSummary, SkillSummary } from '../../src/types'; +import type { DesignSystemSummary, ProjectTemplate, SkillSummary } from '../../src/types'; const skills: SkillSummary[] = [ { @@ -24,6 +24,16 @@ const skills: SkillSummary[] = [ }, ]; +const templates: ProjectTemplate[] = [ + { + id: 'tmpl-landing', + name: 'Landing Page', + description: 'A saved landing page starter.', + files: [{ name: 'prototype/App.jsx', path: 'prototype/App.jsx' }], + createdAt: '2026-05-07T00:00:00.000Z', + }, +]; + const designSystems: DesignSystemSummary[] = [ { id: 'clay', @@ -76,4 +86,30 @@ describe('NewProjectModal layout', () => { expect(screen.getByTestId('new-project-panel')).toBeTruthy(); expect(screen.getByTestId('create-project')).toBeTruthy(); }); + + it('Escape dismisses template delete confirm without closing the modal', () => { + const onClose = vi.fn(); + render( + {}} + onClose={onClose} + />, + ); + + fireEvent.click(screen.getByRole('tab', { name: 'From template' })); + fireEvent.click(screen.getByLabelText(/delete template landing page/i)); + expect(screen.getByTestId('template-delete-confirm-dialog')).toBeTruthy(); + + fireEvent.keyDown(document, { key: 'Escape' }); + expect(screen.queryByTestId('template-delete-confirm-dialog')).toBeNull(); + expect(screen.getByTestId('new-project-modal')).toBeTruthy(); + expect(onClose).not.toHaveBeenCalled(); + }); }); diff --git a/apps/web/tests/components/NewProjectPanel.test.tsx b/apps/web/tests/components/NewProjectPanel.test.tsx index e694e58911..3a6258d948 100644 --- a/apps/web/tests/components/NewProjectPanel.test.tsx +++ b/apps/web/tests/components/NewProjectPanel.test.tsx @@ -667,8 +667,64 @@ describe('NewProjectPanel template deletion', () => { Element.prototype.scrollIntoView = () => {}; }); - it('calls onDeleteTemplate when user clicks delete button', async () => { + function openTemplateTab() { + fireEvent.click(screen.getByRole('tab', { name: 'From template' })); + } + + function clickTemplateDelete() { + fireEvent.click(screen.getByLabelText(/delete template landing page/i)); + } + + it('confirms before deleting and clears selection on success', async () => { const onDelete = vi.fn().mockResolvedValue(true); + const onCreate = vi.fn(); + const { rerender } = render( + , + ); + + openTemplateTab(); + fireEvent.click(screen.getByText('Landing Page')); + clickTemplateDelete(); + expect(screen.getByTestId('template-delete-confirm-dialog')).toBeTruthy(); + expect(onDelete).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('template-delete-cancel')); + expect(screen.queryByTestId('template-delete-confirm-dialog')).toBeNull(); + expect(screen.getByText('Landing Page')).toBeTruthy(); + expect(onDelete).not.toHaveBeenCalled(); + + clickTemplateDelete(); + fireEvent.click(screen.getByTestId('template-delete-confirm')); + expect(onDelete).toHaveBeenCalledWith('tmpl-landing'); + + rerender( + , + ); + expect(screen.queryByText('Landing Page')).toBeNull(); + expect(screen.getByRole('button', { name: /create from template/i })).toHaveProperty( + 'disabled', + true, + ); + }); + + it('shows an error when delete fails and keeps the template', async () => { + const onDelete = vi.fn().mockResolvedValue(false); render( { />, ); - fireEvent.click(screen.getByRole('tab', { name: 'From template' })); - const deleteBtn = screen.getByLabelText(/delete template/i); - fireEvent.click(deleteBtn); + openTemplateTab(); + clickTemplateDelete(); + fireEvent.click(screen.getByTestId('template-delete-confirm')); expect(onDelete).toHaveBeenCalledWith('tmpl-landing'); + expect(await screen.findByTestId('template-delete-error')).toBeTruthy(); + expect(screen.getByText('Landing Page')).toBeTruthy(); + expect(screen.getByTestId('template-delete-confirm-dialog')).toBeTruthy(); + }); + + it('Escape dismisses delete confirm without calling onDelete', () => { + const onDelete = vi.fn(); + render( + , + ); + + openTemplateTab(); + clickTemplateDelete(); + fireEvent.keyDown(document, { key: 'Escape' }); + expect(screen.queryByTestId('template-delete-confirm-dialog')).toBeNull(); + expect(onDelete).not.toHaveBeenCalled(); + expect(screen.getByText('Landing Page')).toBeTruthy(); }); });