mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): use surface-appropriate noun in plugin/template preview unavailable copy (#3229)
After #2840 wired plugin and design-template 404s into the same "no shipped preview" placeholder the skills tab uses, the placeholder copy still hard-coded "skill" — so users opening a Community/Plugins card whose manifest declares a preview entry that doesn't ship saw "No shipped preview for this skill." on a card that is clearly not a skill. Adds a noun discriminator to PreviewView.unavailable so the placeholder reads with the right word per surface — "this skill" on the Skills tab, "this plugin" on Community/Plugins, "this template" on deck-mode design-templates. Locales gain three new preview.noun* strings (with appropriate per-language demonstrative+article) and the existing unavailable title/body interpolate a {noun} placeholder. Also fixes a CSS gap in .ds-modal-unavailable surfaced by the same path: the title and body divs were collapsing onto a single line under .ds-modal-empty's default flex-row. Mirrors the existing .ds-modal-error column+gap layout. Refs #897, #2840.
This commit is contained in:
parent
055680a67d
commit
bbf4809a7e
24 changed files with 277 additions and 64 deletions
|
|
@ -482,7 +482,7 @@ export function ExamplesTab({ skills: rawSkills, onUsePrompt }: Props) {
|
|||
// it can render a calm "no shipped preview" placeholder
|
||||
// instead of bouncing through the error state. Issue #897.
|
||||
unavailable: unavailableKind
|
||||
? { kind: unavailableKind }
|
||||
? { kind: unavailableKind, noun: 'skill' }
|
||||
: null,
|
||||
deck: previewSkill.mode === 'deck',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,15 +18,19 @@ export interface PreviewView {
|
|||
// button that re-fires onView for this view id, instead of sitting
|
||||
// at the loading state forever. Issue #860.
|
||||
error?: string | null;
|
||||
// Set when the underlying skill ships no HTML preview at all (its
|
||||
// Set when the underlying surface ships no HTML preview at all (its
|
||||
// `od.preview.type` is `image`, `markdown`, etc.). The modal renders
|
||||
// a calm "no shipped preview" placeholder instead of the loading or
|
||||
// error states — fetching `/api/skills/:id/example` for those skills
|
||||
// returns 404 today and the resulting "Couldn't load this example."
|
||||
// copy is misleading. `kind` carries the raw preview-type token so
|
||||
// copy can be shaped per kind ("markdown document", "image asset",
|
||||
// …). Mutually exclusive with `html` and `error`. Issue #897.
|
||||
unavailable?: { kind: string } | null;
|
||||
// error states — fetching `/api/skills/:id/example` (or the symmetric
|
||||
// plugin route) returns 404 today and the resulting "Couldn't load
|
||||
// this example." copy is misleading. `kind` carries the raw
|
||||
// preview-type token so copy can be shaped per kind ("markdown
|
||||
// document", "image asset", …). `noun` carries the surface kind so
|
||||
// the placeholder reads with the right word — "skill" on the Skills
|
||||
// tab, "plugin" on Community/Plugins cards, "template" on
|
||||
// design-template (deck) cards. Mutually exclusive with `html` and
|
||||
// `error`. Issues #897, #2840, #3216.
|
||||
unavailable?: { kind: string; noun?: 'skill' | 'plugin' | 'template' } | null;
|
||||
// Deck previews need deck-aware srcdoc/PDF handling so slide navigation and
|
||||
// print-all-slides behavior survive the sandboxed export path.
|
||||
deck?: boolean;
|
||||
|
|
@ -774,20 +778,42 @@ export function PreviewModal({
|
|||
// 404 into the generic "Couldn't load this example." copy
|
||||
// — misleading, since nothing failed: there's just no
|
||||
// preview to render. Show a calm placeholder pointing the
|
||||
// user at "Use this prompt" instead. Issue #897.
|
||||
<div
|
||||
className="ds-modal-empty ds-modal-unavailable"
|
||||
data-testid="preview-unavailable"
|
||||
>
|
||||
<div className="ds-modal-unavailable-title">
|
||||
{t('preview.unavailableTitle')}
|
||||
</div>
|
||||
<div className="ds-modal-unavailable-body">
|
||||
{t('preview.unavailableBody', {
|
||||
kind: activeUnavailable.kind || 'preview',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
// user at "Use this prompt" instead. Issues #897, #2840.
|
||||
//
|
||||
// `noun` lets the same placeholder read with the right
|
||||
// word per surface — Skills tab, Community/Plugins,
|
||||
// design-template (deck) cards. Defaults to 'skill' so
|
||||
// pre-noun callers keep their existing copy. Issue #3216.
|
||||
(() => {
|
||||
const nounKey = ((): 'preview.nounSkill' | 'preview.nounPlugin' | 'preview.nounTemplate' => {
|
||||
switch (activeUnavailable.noun) {
|
||||
case 'plugin':
|
||||
return 'preview.nounPlugin';
|
||||
case 'template':
|
||||
return 'preview.nounTemplate';
|
||||
case 'skill':
|
||||
default:
|
||||
return 'preview.nounSkill';
|
||||
}
|
||||
})();
|
||||
const noun = t(nounKey);
|
||||
return (
|
||||
<div
|
||||
className="ds-modal-empty ds-modal-unavailable"
|
||||
data-testid="preview-unavailable"
|
||||
>
|
||||
<div className="ds-modal-unavailable-title">
|
||||
{t('preview.unavailableTitle', { noun })}
|
||||
</div>
|
||||
<div className="ds-modal-unavailable-body">
|
||||
{t('preview.unavailableBody', {
|
||||
kind: activeUnavailable.kind || 'preview',
|
||||
noun,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : activeError ? (
|
||||
// Distinct error state so a fetch failure stops looking
|
||||
// like an indefinite "Loading…". The Retry button re-fires
|
||||
|
|
|
|||
|
|
@ -95,7 +95,12 @@ export function PluginExampleDetail({
|
|||
label: t('examples.previewLabel'),
|
||||
html,
|
||||
error,
|
||||
unavailable: unavailableKind ? { kind: unavailableKind } : null,
|
||||
// Pass the surface-appropriate noun so the unavailable placeholder
|
||||
// reads "this plugin" / "this template" instead of falling back to
|
||||
// the legacy skills-only "this skill" copy. Issue #3216.
|
||||
unavailable: unavailableKind
|
||||
? { kind: unavailableKind, noun: isDeck ? 'template' : 'plugin' }
|
||||
: null,
|
||||
deck: isDeck,
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -919,8 +919,11 @@ export const ar: Dict = {
|
|||
'preview.errorTitle': 'تعذّر تحميل هذا المثال.',
|
||||
'preview.errorBody': 'فشل جلب HTML الخاص بالمثال. تأكد من تشغيل Open Design ثم أعد المحاولة.',
|
||||
'preview.retry': 'إعادة المحاولة',
|
||||
'preview.unavailableTitle': 'لا توجد معاينة مرفقة لهذه المهارة.',
|
||||
'preview.unavailableBody': 'هذه المهارة تنتج مخرجات {kind} — شغّل الأمر في المحادثة لإنشاء واحدة.',
|
||||
'preview.unavailableTitle': 'لا توجد معاينة مرفقة لـ{noun}.',
|
||||
'preview.unavailableBody': 'شغّل الأمر في المحادثة لإنشاء مخرجات {kind}.',
|
||||
'preview.nounSkill': 'هذه المهارة',
|
||||
'preview.nounPlugin': 'هذه الإضافة',
|
||||
'preview.nounTemplate': 'هذا القالب',
|
||||
'preview.showSidebar': 'إظهار {label}',
|
||||
'preview.hideSidebar': 'إخفاء {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -807,8 +807,11 @@ export const de: Dict = {
|
|||
'preview.errorTitle': 'Beispiel konnte nicht geladen werden.',
|
||||
'preview.errorBody': 'Das Beispiel-HTML konnte nicht abgerufen werden. Stelle sicher, dass Open Design läuft, und versuche es erneut.',
|
||||
'preview.retry': 'Erneut versuchen',
|
||||
'preview.unavailableTitle': 'Für diesen Skill ist keine Vorschau verfügbar.',
|
||||
'preview.unavailableBody': 'Dieser Skill erzeugt {kind}-Output — führe den Prompt im Chat aus, um etwas zu erzeugen.',
|
||||
'preview.unavailableTitle': 'Für {noun} ist keine Vorschau verfügbar.',
|
||||
'preview.unavailableBody': 'Führe den Prompt im Chat aus, um {kind}-Output zu erzeugen.',
|
||||
'preview.nounSkill': 'diesen Skill',
|
||||
'preview.nounPlugin': 'dieses Plugin',
|
||||
'preview.nounTemplate': 'diese Vorlage',
|
||||
'preview.showSidebar': '{label} einblenden',
|
||||
'preview.hideSidebar': '{label} ausblenden',
|
||||
|
||||
|
|
|
|||
|
|
@ -1542,8 +1542,11 @@ export const en: Dict = {
|
|||
'preview.errorTitle': 'Couldn\'t load this example.',
|
||||
'preview.errorBody': 'The example HTML failed to fetch. Make sure Open Design is running and try again.',
|
||||
'preview.retry': 'Retry',
|
||||
'preview.unavailableTitle': 'No shipped preview for this skill.',
|
||||
'preview.unavailableBody': 'This skill produces {kind} output — run the prompt in chat to generate one.',
|
||||
'preview.unavailableTitle': 'No shipped preview for {noun}.',
|
||||
'preview.unavailableBody': 'Run the prompt in chat to generate {kind} output.',
|
||||
'preview.nounSkill': 'this skill',
|
||||
'preview.nounPlugin': 'this plugin',
|
||||
'preview.nounTemplate': 'this template',
|
||||
'preview.showSidebar': 'Show {label}',
|
||||
'preview.hideSidebar': 'Hide {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -808,8 +808,11 @@ export const esES: Dict = {
|
|||
'preview.errorTitle': 'No se pudo cargar este ejemplo.',
|
||||
'preview.errorBody': 'No se pudo obtener el HTML del ejemplo. Asegúrate de que Open Design esté en ejecución e inténtalo de nuevo.',
|
||||
'preview.retry': 'Reintentar',
|
||||
'preview.unavailableTitle': 'No hay vista previa incluida para esta skill.',
|
||||
'preview.unavailableBody': 'Esta skill genera un resultado {kind} — ejecuta el prompt en el chat para crear uno.',
|
||||
'preview.unavailableTitle': 'No hay vista previa incluida para {noun}.',
|
||||
'preview.unavailableBody': 'Ejecuta el prompt en el chat para generar un resultado {kind}.',
|
||||
'preview.nounSkill': 'esta skill',
|
||||
'preview.nounPlugin': 'este plugin',
|
||||
'preview.nounTemplate': 'esta plantilla',
|
||||
'preview.showSidebar': 'Mostrar {label}',
|
||||
'preview.hideSidebar': 'Ocultar {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -941,8 +941,11 @@ export const fa: Dict = {
|
|||
'preview.errorTitle': 'بارگیری این نمونه ممکن نشد.',
|
||||
'preview.errorBody': 'دریافت HTML نمونه با خطا مواجه شد. مطمئن شوید Open Design در حال اجراست و دوباره تلاش کنید.',
|
||||
'preview.retry': 'تلاش دوباره',
|
||||
'preview.unavailableTitle': 'برای این مهارت پیشنمایش همراهی وجود ندارد.',
|
||||
'preview.unavailableBody': 'این مهارت خروجی {kind} تولید میکند — برای ساخت یکی، پرامپت را در گفتگو اجرا کنید.',
|
||||
'preview.unavailableTitle': 'برای {noun} پیشنمایش همراهی وجود ندارد.',
|
||||
'preview.unavailableBody': 'برای ساخت خروجی {kind}، پرامپت را در گفتگو اجرا کنید.',
|
||||
'preview.nounSkill': 'این مهارت',
|
||||
'preview.nounPlugin': 'این افزونه',
|
||||
'preview.nounTemplate': 'این الگو',
|
||||
'preview.showSidebar': 'نمایش {label}',
|
||||
'preview.hideSidebar': 'پنهان کردن {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -1451,8 +1451,11 @@ export const fr: Dict = {
|
|||
'preview.errorTitle': 'Impossible de charger cet exemple.',
|
||||
'preview.errorBody': 'Le chargement du HTML de l\'exemple a échoué. Vérifiez qu\'Open Design est en cours d\'exécution et réessayez.',
|
||||
'preview.retry': 'Réessayer',
|
||||
'preview.unavailableTitle': 'Aucun aperçu fourni pour cette compétence.',
|
||||
'preview.unavailableBody': 'Cette compétence produit un résultat {kind} — exécutez le prompt dans le chat pour en générer un.',
|
||||
'preview.unavailableTitle': 'Aucun aperçu fourni pour {noun}.',
|
||||
'preview.unavailableBody': 'Exécutez le prompt dans le chat pour générer un résultat {kind}.',
|
||||
'preview.nounSkill': 'cette compétence',
|
||||
'preview.nounPlugin': 'ce plugin',
|
||||
'preview.nounTemplate': 'ce modèle',
|
||||
'preview.showSidebar': 'Afficher {label}',
|
||||
'preview.hideSidebar': 'Masquer {label}',
|
||||
'misc.savedTemplate': 'Modèle enregistré',
|
||||
|
|
|
|||
|
|
@ -919,8 +919,11 @@ export const hu: Dict = {
|
|||
'preview.errorTitle': 'A példa betöltése nem sikerült.',
|
||||
'preview.errorBody': 'A példa HTML-jének letöltése meghiúsult. Győződj meg róla, hogy az Open Design fut, majd próbáld újra.',
|
||||
'preview.retry': 'Újra',
|
||||
'preview.unavailableTitle': 'Ehhez a skillhez nincs mellékelt előnézet.',
|
||||
'preview.unavailableBody': 'Ez a skill {kind} kimenetet készít — futtasd a promptot a csevegésben egy létrehozásához.',
|
||||
'preview.unavailableTitle': 'Nincs mellékelt előnézet: {noun}.',
|
||||
'preview.unavailableBody': 'Futtasd a promptot a csevegésben, hogy {kind} kimenetet készíts.',
|
||||
'preview.nounSkill': 'ez a skill',
|
||||
'preview.nounPlugin': 'ez a plugin',
|
||||
'preview.nounTemplate': 'ez a sablon',
|
||||
'preview.showSidebar': '{label} megjelenítése',
|
||||
'preview.hideSidebar': '{label} elrejtése',
|
||||
|
||||
|
|
|
|||
|
|
@ -1030,8 +1030,11 @@ export const id: Dict = {
|
|||
'preview.errorTitle': 'Tidak dapat memuat contoh ini.',
|
||||
'preview.errorBody': 'Pengambilan HTML contoh gagal. Pastikan Open Design berjalan, lalu coba lagi.',
|
||||
'preview.retry': 'Coba lagi',
|
||||
'preview.unavailableTitle': 'Tidak ada pratinjau bawaan untuk skill ini.',
|
||||
'preview.unavailableBody': 'Skill ini menghasilkan keluaran {kind} — jalankan prompt di chat untuk membuatnya.',
|
||||
'preview.unavailableTitle': 'Tidak ada pratinjau bawaan untuk {noun}.',
|
||||
'preview.unavailableBody': 'Jalankan prompt di chat untuk membuat keluaran {kind}.',
|
||||
'preview.nounSkill': 'skill ini',
|
||||
'preview.nounPlugin': 'plugin ini',
|
||||
'preview.nounTemplate': 'templat ini',
|
||||
'preview.showSidebar': 'Tampilkan {label}',
|
||||
'preview.hideSidebar': 'Sembunyikan {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -834,8 +834,11 @@ export const it: Dict = {
|
|||
'preview.errorTitle': 'Impossibile caricare questo esempio.',
|
||||
'preview.errorBody': 'Il caricamento dell\'HTML dell\'esempio è fallito. Verifica che Open Design sia in esecuzione e riprova.',
|
||||
'preview.retry': 'Riprova',
|
||||
'preview.unavailableTitle': 'Nessuna anteprima fornita per questa competenza.',
|
||||
'preview.unavailableBody': 'Questa competenza produce un risultato {kind} — esegui il prompt nella chat per generarne uno.',
|
||||
'preview.unavailableTitle': 'Nessuna anteprima fornita per {noun}.',
|
||||
'preview.unavailableBody': 'Esegui il prompt nella chat per generare un risultato {kind}.',
|
||||
'preview.nounSkill': 'questa competenza',
|
||||
'preview.nounPlugin': 'questo plugin',
|
||||
'preview.nounTemplate': 'questo modello',
|
||||
'preview.showSidebar': 'Mostra {label}',
|
||||
'preview.hideSidebar': 'Nascondi {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -806,8 +806,11 @@ export const ja: Dict = {
|
|||
'preview.errorTitle': 'この例を読み込めませんでした。',
|
||||
'preview.errorBody': '例の HTML を取得できませんでした。Open Design が起動していることを確認して再試行してください。',
|
||||
'preview.retry': '再試行',
|
||||
'preview.unavailableTitle': 'このスキルにはプレビューが同梱されていません。',
|
||||
'preview.unavailableBody': 'このスキルは {kind} 出力を生成します — チャットでプロンプトを実行して生成してください。',
|
||||
'preview.unavailableTitle': '{noun}にはプレビューが同梱されていません。',
|
||||
'preview.unavailableBody': 'チャットでプロンプトを実行して {kind} 出力を生成してください。',
|
||||
'preview.nounSkill': 'このスキル',
|
||||
'preview.nounPlugin': 'このプラグイン',
|
||||
'preview.nounTemplate': 'このテンプレート',
|
||||
'preview.showSidebar': '{label} を表示',
|
||||
'preview.hideSidebar': '{label} を非表示',
|
||||
|
||||
|
|
|
|||
|
|
@ -919,8 +919,11 @@ export const ko: Dict = {
|
|||
'preview.errorTitle': '이 예제를 불러오지 못했습니다.',
|
||||
'preview.errorBody': '예제 HTML을 가져오지 못했습니다. Open Design이 실행 중인지 확인하고 다시 시도하세요.',
|
||||
'preview.retry': '다시 시도',
|
||||
'preview.unavailableTitle': '이 스킬에는 함께 제공되는 미리보기가 없습니다.',
|
||||
'preview.unavailableBody': '이 스킬은 {kind} 출력을 생성합니다 — 채팅에서 프롬프트를 실행해 생성하세요.',
|
||||
'preview.unavailableTitle': '{noun}에는 함께 제공되는 미리보기가 없습니다.',
|
||||
'preview.unavailableBody': '채팅에서 프롬프트를 실행해 {kind} 출력을 생성하세요.',
|
||||
'preview.nounSkill': '이 스킬',
|
||||
'preview.nounPlugin': '이 플러그인',
|
||||
'preview.nounTemplate': '이 템플릿',
|
||||
'preview.showSidebar': '{label} 표시',
|
||||
'preview.hideSidebar': '{label} 숨기기',
|
||||
|
||||
|
|
|
|||
|
|
@ -919,8 +919,11 @@ export const pl: Dict = {
|
|||
'preview.errorTitle': 'Nie udało się załadować tego przykładu.',
|
||||
'preview.errorBody': 'Nie udało się pobrać kodu HTML przykładu. Upewnij się, że Open Design jest uruchomiony, i spróbuj ponownie.',
|
||||
'preview.retry': 'Spróbuj ponownie',
|
||||
'preview.unavailableTitle': 'Brak dołączonego podglądu dla tej umiejętności.',
|
||||
'preview.unavailableBody': 'Ta umiejętność tworzy {kind} wynik — uruchom prompt w czacie, aby go wygenerować.',
|
||||
'preview.unavailableTitle': 'Brak dołączonego podglądu dla {noun}.',
|
||||
'preview.unavailableBody': 'Uruchom prompt w czacie, aby wygenerować {kind} wynik.',
|
||||
'preview.nounSkill': 'tej umiejętności',
|
||||
'preview.nounPlugin': 'tego pluginu',
|
||||
'preview.nounTemplate': 'tego szablonu',
|
||||
'preview.showSidebar': 'Pokaż {label}',
|
||||
'preview.hideSidebar': 'Ukryj {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -940,8 +940,11 @@ export const ptBR: Dict = {
|
|||
'preview.errorTitle': 'Não foi possível carregar este exemplo.',
|
||||
'preview.errorBody': 'A obtenção do HTML do exemplo falhou. Verifique se o Open Design está em execução e tente novamente.',
|
||||
'preview.retry': 'Tentar novamente',
|
||||
'preview.unavailableTitle': 'Nenhuma prévia incluída para esta skill.',
|
||||
'preview.unavailableBody': 'Esta skill produz um resultado {kind} — execute o prompt no chat para gerar um.',
|
||||
'preview.unavailableTitle': 'Nenhuma prévia incluída para {noun}.',
|
||||
'preview.unavailableBody': 'Execute o prompt no chat para gerar um resultado {kind}.',
|
||||
'preview.nounSkill': 'esta skill',
|
||||
'preview.nounPlugin': 'este plugin',
|
||||
'preview.nounTemplate': 'este modelo',
|
||||
'preview.showSidebar': 'Mostrar {label}',
|
||||
'preview.hideSidebar': 'Ocultar {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -940,8 +940,11 @@ export const ru: Dict = {
|
|||
'preview.errorTitle': 'Не удалось загрузить этот пример.',
|
||||
'preview.errorBody': 'Не удалось получить HTML примера. Убедитесь, что Open Design запущен, и повторите попытку.',
|
||||
'preview.retry': 'Повторить',
|
||||
'preview.unavailableTitle': 'Для этого навыка нет встроенного предпросмотра.',
|
||||
'preview.unavailableBody': 'Этот навык создаёт {kind}-вывод — запустите запрос в чате, чтобы сгенерировать его.',
|
||||
'preview.unavailableTitle': 'Для {noun} нет встроенного предпросмотра.',
|
||||
'preview.unavailableBody': 'Запустите запрос в чате, чтобы сгенерировать {kind}-вывод.',
|
||||
'preview.nounSkill': 'этого навыка',
|
||||
'preview.nounPlugin': 'этого плагина',
|
||||
'preview.nounTemplate': 'этого шаблона',
|
||||
'preview.showSidebar': 'Показать {label}',
|
||||
'preview.hideSidebar': 'Скрыть {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -908,8 +908,11 @@ export const tr: Dict = {
|
|||
'preview.errorTitle': 'Bu örnek yüklenemedi.',
|
||||
'preview.errorBody': 'Örnek HTML\'i alınamadı. Open Design\'ın çalıştığından emin olup tekrar deneyin.',
|
||||
'preview.retry': 'Tekrar dene',
|
||||
'preview.unavailableTitle': 'Bu yetenek için birlikte gelen bir önizleme yok.',
|
||||
'preview.unavailableBody': 'Bu yetenek {kind} çıktısı üretir — bir tane oluşturmak için sohbette istemini çalıştırın.',
|
||||
'preview.unavailableTitle': '{noun} için birlikte gelen bir önizleme yok.',
|
||||
'preview.unavailableBody': 'Sohbette istemini çalıştırarak {kind} çıktısı üretin.',
|
||||
'preview.nounSkill': 'bu yetenek',
|
||||
'preview.nounPlugin': 'bu eklenti',
|
||||
'preview.nounTemplate': 'bu şablon',
|
||||
'preview.showSidebar': '{label} göster',
|
||||
'preview.hideSidebar': '{label} gizle',
|
||||
|
||||
|
|
|
|||
|
|
@ -941,8 +941,11 @@ export const uk: Dict = {
|
|||
'preview.errorTitle': 'Не вдалося завантажити цей приклад.',
|
||||
'preview.errorBody': 'Не вдалося отримати HTML прикладу. Переконайтеся, що Open Design запущено, і повторіть спробу.',
|
||||
'preview.retry': 'Повторити',
|
||||
'preview.unavailableTitle': 'Для цієї навички немає вбудованого попереднього перегляду.',
|
||||
'preview.unavailableBody': 'Ця навичка створює {kind}-вивід — запустіть підказку в чаті, щоб його згенерувати.',
|
||||
'preview.unavailableTitle': 'Для {noun} немає вбудованого попереднього перегляду.',
|
||||
'preview.unavailableBody': 'Запустіть підказку в чаті, щоб згенерувати {kind}-вивід.',
|
||||
'preview.nounSkill': 'цієї навички',
|
||||
'preview.nounPlugin': 'цього плагіна',
|
||||
'preview.nounTemplate': 'цього шаблону',
|
||||
'preview.showSidebar': 'Показати {label}',
|
||||
'preview.hideSidebar': 'Приховати {label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -1533,8 +1533,11 @@ export const zhCN: Dict = {
|
|||
'preview.errorTitle': '无法加载此示例。',
|
||||
'preview.errorBody': '示例 HTML 加载失败。请确认 Open Design 正在运行后重试。',
|
||||
'preview.retry': '重试',
|
||||
'preview.unavailableTitle': '此技能暂未附带预览样例。',
|
||||
'preview.unavailableBody': '此技能用于生成 {kind} 产物 — 请在对话中运行此 Prompt 来生成。',
|
||||
'preview.unavailableTitle': '{noun}暂未附带预览样例。',
|
||||
'preview.unavailableBody': '请在对话中运行此 Prompt 来生成 {kind} 产物。',
|
||||
'preview.nounSkill': '此技能',
|
||||
'preview.nounPlugin': '此插件',
|
||||
'preview.nounTemplate': '此模板',
|
||||
'preview.showSidebar': '展开{label}',
|
||||
'preview.hideSidebar': '收起{label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -1133,8 +1133,11 @@ export const zhTW: Dict = {
|
|||
'preview.errorTitle': '無法載入此範例。',
|
||||
'preview.errorBody': '範例 HTML 載入失敗。請確認 Open Design 正在執行後重試。',
|
||||
'preview.retry': '重試',
|
||||
'preview.unavailableTitle': '此技能尚未附帶預覽範例。',
|
||||
'preview.unavailableBody': '此技能用於產生 {kind} 產物 — 請在對話中執行此 Prompt 來產生。',
|
||||
'preview.unavailableTitle': '{noun}尚未附帶預覽範例。',
|
||||
'preview.unavailableBody': '請在對話中執行此 Prompt 來產生 {kind} 產物。',
|
||||
'preview.nounSkill': '此技能',
|
||||
'preview.nounPlugin': '此外掛',
|
||||
'preview.nounTemplate': '此範本',
|
||||
'preview.showSidebar': '展開{label}',
|
||||
'preview.hideSidebar': '收合{label}',
|
||||
|
||||
|
|
|
|||
|
|
@ -1851,13 +1851,23 @@ export interface Dict {
|
|||
'preview.errorTitle': string;
|
||||
'preview.errorBody': string;
|
||||
'preview.retry': string;
|
||||
// Friendly placeholder copy for skills whose `od.preview.type` is not
|
||||
// `html` — they ship no fetchable example artifact, so the loading /
|
||||
// error states are misleading. Issue #897.
|
||||
// Friendly placeholder copy for surfaces whose `od.preview.type` is
|
||||
// not `html`, or whose manifest declares a preview entry that doesn't
|
||||
// ship on disk — they have no fetchable example artifact, so the
|
||||
// loading / error states are misleading. Issues #897, #2840, #3216.
|
||||
// Body uses the `{kind}` placeholder (raw `od.preview.type` token,
|
||||
// e.g. "markdown" or "image"); both keys use the `{noun}` placeholder
|
||||
// so the same wording reads correctly on skills, plugins, and design
|
||||
// templates (filled from one of the `preview.noun.*` keys below).
|
||||
'preview.unavailableTitle': string;
|
||||
// Body copy uses the `{kind}` placeholder (raw `od.preview.type`
|
||||
// token, e.g. "markdown" or "image") so each kind reads naturally.
|
||||
'preview.unavailableBody': string;
|
||||
// Noun variants so the unavailable placeholder reads with the right
|
||||
// word for each surface — Skills tab vs. Community/Plugins vs. deck
|
||||
// design-templates. Keep these short, capitalised by the host
|
||||
// language's conventions, and translatable in every locale.
|
||||
'preview.nounSkill': string;
|
||||
'preview.nounPlugin': string;
|
||||
'preview.nounTemplate': string;
|
||||
'preview.showSidebar': string;
|
||||
'preview.hideSidebar': string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1436,6 +1436,27 @@
|
|||
max-width: 48ch;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/* Unavailable state mirrors .ds-modal-error's column layout so the
|
||||
title and body stack with a visible gap instead of collapsing onto a
|
||||
single line under the parent flex-row default. Surfaced on the plugin
|
||||
path after #2840 wired plugin 404s into this placeholder. Issue #3216. */
|
||||
.ds-modal-unavailable {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 0 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.ds-modal-unavailable-title {
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ds-modal-unavailable-body {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
max-width: 48ch;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.ds-modal-actions .ghost.is-active {
|
||||
background: var(--accent-tint);
|
||||
color: var(--accent);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
// Regression for nexu-io/open-design#3216: after #2840 wired plugin and
|
||||
// design-template 404s into the same "no shipped preview" placeholder the
|
||||
// skills tab uses, the placeholder copy still hard-coded "skill" — so a user
|
||||
// opening a Community/Plugins card whose manifest declares a preview entry
|
||||
// that doesn't ship saw "No shipped preview for this skill." on a card that
|
||||
// is clearly not a skill. Lock the noun-per-surface contract by asserting
|
||||
// that PluginExampleDetail's unavailable copy reads with the right noun.
|
||||
//
|
||||
// Plugin records (non-deck) read "plugin". Deck-mode records read "template".
|
||||
// The skills consumer (ExamplesTab) keeps the existing "skill" wording and is
|
||||
// covered by the existing preview-modal-unavailable-state suite.
|
||||
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { InstalledPluginRecord } from '@open-design/contracts';
|
||||
|
||||
import { PluginExampleDetail } from '../../src/components/plugin-details/PluginExampleDetail';
|
||||
|
||||
vi.mock('../../src/providers/registry', () => ({
|
||||
fetchPluginPreviewHtml: vi.fn(async () => ({ unavailable: true, kind: 'html' })),
|
||||
fetchPluginExampleHtml: vi.fn(async () => ({ unavailable: true, kind: 'html' })),
|
||||
}));
|
||||
|
||||
function make(overrides: {
|
||||
id: string;
|
||||
title?: string;
|
||||
mode?: string;
|
||||
}): InstalledPluginRecord {
|
||||
return {
|
||||
id: overrides.id,
|
||||
title: overrides.title ?? overrides.id,
|
||||
version: '0.1.0',
|
||||
sourceKind: 'bundled',
|
||||
source: '/tmp',
|
||||
trust: 'bundled',
|
||||
capabilitiesGranted: [],
|
||||
manifest: {
|
||||
name: overrides.id,
|
||||
version: '0.1.0',
|
||||
title: overrides.title ?? overrides.id,
|
||||
od: {
|
||||
kind: 'scenario',
|
||||
...(overrides.mode ? { mode: overrides.mode } : {}),
|
||||
preview: { type: 'html', entry: './missing.html' },
|
||||
},
|
||||
},
|
||||
fsPath: '/tmp',
|
||||
installedAt: 0,
|
||||
updatedAt: 0,
|
||||
};
|
||||
}
|
||||
|
||||
describe('PluginExampleDetail unavailable-state noun', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads as "plugin" when a non-deck plugin ships no preview', async () => {
|
||||
render(
|
||||
<PluginExampleDetail
|
||||
record={make({ id: 'example-live-artifact', title: 'Live Artifact' })}
|
||||
onClose={() => {}}
|
||||
onUse={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const placeholder = await waitFor(() => screen.getByTestId('preview-unavailable'));
|
||||
const copy = placeholder.textContent ?? '';
|
||||
|
||||
// The noun must match the card surface — calling a plugin a "skill" is
|
||||
// what #3216 was filed for.
|
||||
expect(copy).toMatch(/plugin/i);
|
||||
expect(copy).not.toMatch(/\bskill\b/i);
|
||||
});
|
||||
|
||||
it('reads as "template" when a deck-mode plugin ships no preview', async () => {
|
||||
render(
|
||||
<PluginExampleDetail
|
||||
record={make({ id: 'replit-deck', title: 'Replit Deck', mode: 'deck' })}
|
||||
onClose={() => {}}
|
||||
onUse={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const placeholder = await waitFor(() => screen.getByTestId('preview-unavailable'));
|
||||
const copy = placeholder.textContent ?? '';
|
||||
|
||||
// Decks are surfaced as design templates in Home → Community, so the
|
||||
// copy should track that vocabulary instead of saying "plugin" or
|
||||
// "skill".
|
||||
expect(copy).toMatch(/template/i);
|
||||
expect(copy).not.toMatch(/\bskill\b/i);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue