feat(web): wire design-system gallery to create panel and prompt templates

Single entry-point flow: picking a design system from the gallery now
flows through to the create panel and prompt-template galleries instead
of just stamping config and stopping.

- DesignSystemsTab: replace whole-card click-to-select with an explicit
  "Use for new project" button on each card. Thumb stays clickable for
  preview; cursor only becomes "pointer" on the thumb and the action
  button so the card body no longer suggests a hidden click target.
- EntryView: new selectDesignSystemFromGallery handler that updates the
  workspace default DS and, when the DS surface is image/video, jumps to
  the matching prompt-template top tab (image-templates / video-templates).
- NewProjectPanel: when the workspace default DS changes from the gallery,
  mirror it into the picker primary slot, drop multi-select, and clear
  the dsSelectionTouched flag so the existing default-sync effect keeps
  things aligned afterwards. Media-aligned DS additionally switches the
  create tab to image/video so the sidebar matches the gallery.
- PromptTemplatesTab: accept designSystemId/designSystems and narrow the
  visible cards to categories implied by the DS metadata when no explicit
  category filter is set. Show a dismissible hint
  ("Showing templates related to ...") with a "Show all templates"
  escape hatch. Templates themselves stay surface-grouped; only the
  visible card set narrows.
- New util utils/promptTemplateDsCategories.ts maps DS title/category/
  summary to a curated set of prompt-template categories, kept here so
  PromptTemplatesTab does not couple to per-template schema.
- i18n: add promptTemplates.dsNarrowedHint, promptTemplates.showAllTemplates,
  and ds.selectForProject across all 20 locales (+ types.ts).
- index.css: styles for .ds-card-actions / .ds-card-select-btn and
  .prompt-templates-ds-hint / -clear; switch ds-card cursor from pointer
  to default and move pointer onto .ds-card-thumb.

Validated with pnpm guard, pnpm --filter @open-design/web typecheck, and
pnpm --filter @open-design/web test (392/392 pass, incl. locale-key parity).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pftom
2026-05-08 09:25:10 +08:00
co-authored by Cursor
parent 2bb029cb58
commit fbb22f5f3e
24 changed files with 284 additions and 18 deletions
+14 -13
View File
@@ -223,19 +223,7 @@ function DesignSystemCard({
);
return (
<div
ref={ref}
className={`ds-card ${active ? 'active' : ''}`}
role="button"
tabIndex={0}
onClick={onSelect}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelect();
}
}}
>
<div ref={ref} className={`ds-card ${active ? 'active' : ''}`}>
<div
className="ds-card-thumb"
onClick={(e) => {
@@ -298,6 +286,19 @@ function DesignSystemCard({
</div>
) : null}
</div>
<div className="ds-card-actions">
<button
type="button"
className="ds-card-select-btn"
data-testid="design-system-select"
onClick={(e) => {
e.stopPropagation();
onSelect();
}}
>
{t('ds.selectForProject')}
</button>
</div>
</div>
</div>
);
+17 -1
View File
@@ -299,6 +299,18 @@ export function EntryView({
setPreviewSystemId(id);
}
/** Persist workspace default, mirror sidebar picker state, and jump to media galleries when relevant. */
function selectDesignSystemFromGallery(id: string) {
onChangeDefaultDesignSystem(id);
const ds = designSystems.find((d) => d.id === id);
const surf = ds?.surface ?? 'web';
if (surf === 'image') {
setTopTab('image-templates');
} else if (surf === 'video') {
setTopTab('video-templates');
}
}
const previewSystem = useMemo(
() => (previewSystemId ? designSystems.find((d) => d.id === previewSystemId) ?? null : null),
[designSystems, previewSystemId],
@@ -634,7 +646,7 @@ export function EntryView({
<DesignSystemsTab
systems={designSystems}
selectedId={defaultDesignSystemId}
onSelect={onChangeDefaultDesignSystem}
onSelect={selectDesignSystemFromGallery}
onPreview={previewDesignSystem}
/>
) : null}
@@ -654,6 +666,8 @@ export function EntryView({
<PromptTemplatesTab
surface="image"
templates={promptTemplates}
designSystemId={defaultDesignSystemId}
designSystems={designSystems}
onPreview={setPreviewPromptTemplate}
/>
) : null}
@@ -661,6 +675,8 @@ export function EntryView({
<PromptTemplatesTab
surface="video"
templates={promptTemplates}
designSystemId={defaultDesignSystemId}
designSystems={designSystems}
onPreview={setPreviewPromptTemplate}
/>
) : null}
@@ -294,6 +294,27 @@ export function NewProjectPanel({
setSelectedDsIds(ids);
}
// Selecting a design system from the gallery is an explicit intent that
// should win over any earlier in-panel picker tweaks: mirror it into the
// primary slot, drop multi-select, and clear the touched flag so the
// existing default-sync effect above keeps things aligned afterwards.
useEffect(() => {
if (!defaultDesignSystemId) return;
setSelectedDsIds([defaultDesignSystemId]);
setDsMulti(false);
setDsSelectionTouched(false);
}, [defaultDesignSystemId]);
// Media-oriented design systems pair with their create tabs so the sidebar
// matches what the image/video template galleries imply.
useEffect(() => {
if (!defaultDesignSystemId) return;
const ds = designSystems.find((d) => d.id === defaultDesignSystemId);
const surf = ds?.surface ?? 'web';
if (surf === 'image') setTab('image');
else if (surf === 'video') setTab('video');
}, [defaultDesignSystemId, designSystems]);
useEffect(() => {
const el = tabsRef.current;
if (!el) return;
+55 -3
View File
@@ -4,7 +4,12 @@ import {
localizePromptTemplateCategory,
localizePromptTemplateSummary,
} from '../i18n/content';
import type { PromptTemplateSource, PromptTemplateSummary } from '../types';
import type {
DesignSystemSummary,
PromptTemplateSource,
PromptTemplateSummary,
} from '../types';
import { inferPromptTemplateCategoriesForDs } from '../utils/promptTemplateDsCategories';
import { Icon } from './Icon';
// Stable, human-readable provider name used by the source filter and the
@@ -27,6 +32,11 @@ function providerLabel(source: PromptTemplateSource): string {
interface Props {
surface: 'image' | 'video';
templates: PromptTemplateSummary[];
// When the workspace default design system is media-aligned, pass it in to
// narrow the gallery to categories implied by the DS metadata. Templates
// themselves stay surface-grouped; only the visible card set narrows.
designSystemId?: string | null;
designSystems?: DesignSystemSummary[];
onPreview: (tpl: PromptTemplateSummary) => void;
}
@@ -35,11 +45,34 @@ interface Props {
// card grid that lazy-loads remote thumbnails (the upstream README hosts
// images on CMS / Cloudflare Stream, both public). Each card opens a
// preview modal with the full prompt body and attribution.
export function PromptTemplatesTab({ surface, templates, onPreview }: Props) {
export function PromptTemplatesTab({
surface,
templates,
designSystemId,
designSystems,
onPreview,
}: Props) {
const { locale, t } = useI18n();
const [filter, setFilter] = useState('');
const [category, setCategory] = useState<string>('All');
const [source, setSource] = useState<string>('All');
// When the user explicitly clears the DS narrowing, the hint disappears
// until they pick a different DS (which remounts the inferred set).
const [showAll, setShowAll] = useState(false);
const activeDs = useMemo(() => {
if (!designSystemId || !designSystems) return null;
return designSystems.find((d) => d.id === designSystemId) ?? null;
}, [designSystemId, designSystems]);
const dsNarrowCategories = useMemo(() => {
if (!activeDs) return null;
const inferred = inferPromptTemplateCategoriesForDs(activeDs);
if (!inferred || inferred.length === 0) return null;
return new Set(inferred.map((c) => c.toLowerCase()));
}, [activeDs]);
const dsNarrowActive = !!dsNarrowCategories && !showAll && category === 'All';
const surfaceScoped = useMemo(
() => templates.filter((tpl) => tpl.surface === surface),
@@ -67,6 +100,13 @@ export function PromptTemplatesTab({ surface, templates, onPreview }: Props) {
if (category !== 'All' && (tpl.category || 'General') !== category) {
return false;
}
if (
dsNarrowActive
&& dsNarrowCategories
&& !dsNarrowCategories.has((tpl.category || 'General').toLowerCase())
) {
return false;
}
if (source !== 'All' && providerLabel(tpl.source) !== source) {
return false;
}
@@ -83,7 +123,7 @@ export function PromptTemplatesTab({ surface, templates, onPreview }: Props) {
|| providerLabel(tpl.source).toLowerCase().includes(q)
);
});
}, [surfaceScoped, filter, category, source, locale]);
}, [surfaceScoped, filter, category, source, locale, dsNarrowActive, dsNarrowCategories]);
if (surfaceScoped.length === 0) {
return (
@@ -97,6 +137,18 @@ export function PromptTemplatesTab({ surface, templates, onPreview }: Props) {
return (
<div className="tab-panel prompt-templates-panel">
{dsNarrowActive && activeDs ? (
<div className="prompt-templates-ds-hint">
<span>{t('promptTemplates.dsNarrowedHint', { title: activeDs.title })}</span>
<button
type="button"
className="prompt-templates-ds-hint-clear"
onClick={() => setShowAll(true)}
>
{t('promptTemplates.showAllTemplates')}
</button>
</div>
) : null}
<div className="tab-panel-toolbar">
<input
placeholder={t('promptTemplates.searchPlaceholder')}
+3
View File
@@ -173,6 +173,8 @@ export const ar: Dict = {
'promptTemplates.allSources': 'جميع المصادر',
'promptTemplates.sourceFilterAria': 'تصفية حسب المصدر',
'promptTemplates.attributionFooter': 'مقتبس من مكتبات الأوامر العامة. كل بطاقة تشير إلى المؤلف الأصلي.',
'promptTemplates.dsNarrowedHint': 'عرض القوالب المتعلقة بـ «{title}».',
'promptTemplates.showAllTemplates': 'عرض كل القوالب',
'promptTemplates.openPreviewTitle': 'فتح الأمر والمعاينة',
'promptTemplates.sourcePrefix': 'المصدر:',
'promptTemplates.fetchError': 'تعذر تحميل جسم هذا القالب.',
@@ -399,6 +401,7 @@ export const ar: Dict = {
'ds.tokens': 'الرموز',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'جاري تحميل DESIGN.md...',
'ds.selectForProject': 'استخدام في مشروع جديد',
'avatar.title': 'الحساب والإعدادات',
'avatar.localCli': 'CLI محلي',
+3
View File
@@ -170,6 +170,8 @@ export const de: Dict = {
'promptTemplates.emptyVideo': 'Noch keine Video-Prompt-Templates installiert.',
'promptTemplates.emptyNoMatch': 'Keine Templates passen zu Ihrer Suche.',
'promptTemplates.attributionFooter': 'Aus öffentlichen Prompt-Bibliotheken adaptiert. Jede Karte verlinkt zum ursprünglichen Autor.',
'promptTemplates.dsNarrowedHint': 'Zeigt Vorlagen im Kontext von „{title}“.',
'promptTemplates.showAllTemplates': 'Alle Vorlagen anzeigen',
'promptTemplates.openPreviewTitle': 'Prompt und Vorschau öffnen',
'promptTemplates.sourcePrefix': 'Quelle:',
'promptTemplates.fetchError': 'Dieser Template-Inhalt konnte nicht geladen werden.',
@@ -353,6 +355,7 @@ export const de: Dict = {
'ds.tokens': 'Tokens',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'DESIGN.md wird geladen…',
'ds.selectForProject': 'Für neues Projekt verwenden',
'avatar.title': 'Konto & Einstellungen',
'avatar.localCli': 'Lokale CLI',
+3
View File
@@ -169,6 +169,8 @@ export const en: Dict = {
'promptTemplates.emptyVideo': 'No video prompt templates installed yet.',
'promptTemplates.emptyNoMatch': 'No templates match your search.',
'promptTemplates.attributionFooter': 'Adapted from public prompt libraries. Each card links back to the original author.',
'promptTemplates.dsNarrowedHint': 'Showing templates related to “{title}”.',
'promptTemplates.showAllTemplates': 'Show all templates',
'promptTemplates.openPreviewTitle': 'Open prompt and preview',
'promptTemplates.sourcePrefix': 'Source:',
'promptTemplates.fetchError': 'Could not load this template body.',
@@ -410,6 +412,7 @@ export const en: Dict = {
'ds.tokens': 'Tokens',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'Loading DESIGN.md…',
'ds.selectForProject': 'Use for new project',
'avatar.title': 'Account & settings',
'avatar.localCli': 'Local CLI',
+3
View File
@@ -171,6 +171,8 @@ export const esES: Dict = {
'promptTemplates.emptyNoMatch': 'Ninguna plantilla coincide con tu búsqueda.',
'promptTemplates.attributionFooter':
'Adaptadas de bibliotecas públicas de prompts. Cada tarjeta enlaza al autor original.',
'promptTemplates.dsNarrowedHint': 'Mostrando plantillas relacionadas con «{title}».',
'promptTemplates.showAllTemplates': 'Mostrar todas las plantillas',
'promptTemplates.openPreviewTitle': 'Abrir prompt y vista previa',
'promptTemplates.sourcePrefix': 'Fuente:',
'promptTemplates.fetchError': 'No se pudo cargar el cuerpo de esta plantilla.',
@@ -354,6 +356,7 @@ export const esES: Dict = {
'ds.tokens': 'Tokens',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'Cargando DESIGN.md…',
'ds.selectForProject': 'Usar en proyecto nuevo',
'avatar.title': 'Cuenta y ajustes',
'avatar.localCli': 'CLI local',
+3
View File
@@ -170,6 +170,8 @@ export const fa: Dict = {
'promptTemplates.emptyNoMatch': 'هیچ قالبی با جستجوی شما مطابقت ندارد.',
'promptTemplates.attributionFooter':
'برگرفته از کتابخانه‌های عمومی پرامپت. هر کارت به نویسنده اصلی لینک دارد.',
'promptTemplates.dsNarrowedHint': 'نمایش قالب‌های مرتبط با «{title}».',
'promptTemplates.showAllTemplates': 'نمایش همه قالب‌ها',
'promptTemplates.openPreviewTitle': 'باز کردن پرامپت و پیش‌نمایش',
'promptTemplates.sourcePrefix': 'منبع:',
'promptTemplates.fetchError': 'بارگذاری متن این قالب ممکن نبود.',
@@ -410,6 +412,7 @@ export const fa: Dict = {
'ds.tokens': 'توکن‌ها',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'بارگذاری DESIGN.md…',
'ds.selectForProject': 'استفاده در پروژه جدید',
'avatar.title': 'حساب و تنظیمات',
'avatar.localCli': 'CLI محلی',
+3
View File
@@ -173,6 +173,8 @@ export const fr: Dict = {
'promptTemplates.allSources': 'Toutes les sources',
'promptTemplates.sourceFilterAria': 'Filtrer par source',
'promptTemplates.attributionFooter': 'Adapté de bibliothèques de prompts publiques. Chaque carte renvoie vers l\'auteur original.',
'promptTemplates.dsNarrowedHint': 'Affiche les modèles liés à « {title} ».',
'promptTemplates.showAllTemplates': 'Afficher tous les modèles',
'promptTemplates.openPreviewTitle': 'Ouvrir le prompt et l\'aperçu',
'promptTemplates.sourcePrefix': 'Source :',
'promptTemplates.fetchError': 'Impossible de charger le contenu de ce modèle.',
@@ -399,6 +401,7 @@ export const fr: Dict = {
'ds.tokens': 'Jetons',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'Chargement du DESIGN.md…',
'ds.selectForProject': 'Utiliser pour un nouveau projet',
'avatar.title': 'Compte et paramètres',
'avatar.localCli': 'CLI local',
+3
View File
@@ -171,6 +171,8 @@ export const hu: Dict = {
'promptTemplates.emptyVideo': 'Még nincs telepített videósablon.',
'promptTemplates.emptyNoMatch': 'Egy sablon sem felel meg a keresésnek.',
'promptTemplates.attributionFooter': 'Nyilvános prompt-tárakból átvéve. Minden kártya az eredeti szerzőre mutat.',
'promptTemplates.dsNarrowedHint': 'A(z) „{title}” rendszerhez kapcsolódó sablonok láthatók.',
'promptTemplates.showAllTemplates': 'Összes sablon megjelenítése',
'promptTemplates.openPreviewTitle': 'Prompt és előnézet megnyitása',
'promptTemplates.sourcePrefix': 'Forrás:',
'promptTemplates.fetchError': 'A sablon törzse nem tölthető be.',
@@ -399,6 +401,7 @@ export const hu: Dict = {
'ds.tokens': 'Tokenek',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'DESIGN.md betöltése…',
'ds.selectForProject': 'Új projektben használom',
'avatar.title': 'Fiók és beállítások',
'avatar.localCli': 'Helyi CLI',
+3
View File
@@ -168,6 +168,8 @@ export const id: Dict = {
'promptTemplates.emptyVideo': 'Belum ada templat prompt video.',
'promptTemplates.emptyNoMatch': 'Tidak ada templat yang cocok dengan pencarianmu.',
'promptTemplates.attributionFooter': 'Diadaptasi dari library prompt publik. Tiap kartu menautkan ke author asli.',
'promptTemplates.dsNarrowedHint': 'Menampilkan templat terkait "{title}".',
'promptTemplates.showAllTemplates': 'Tampilkan semua templat',
'promptTemplates.openPreviewTitle': 'Buka prompt dan pratinjau',
'promptTemplates.sourcePrefix': 'Sumber:',
'promptTemplates.fetchError': 'Tidak bisa memuat isi templat ini.',
@@ -404,6 +406,7 @@ export const id: Dict = {
'ds.tokens': 'Token',
'ds.specToggle': 'Spesifikasi',
'ds.specLoading': 'Memuat spesifikasi...',
'ds.selectForProject': 'Gunakan untuk proyek baru',
'avatar.title': 'Akun & pengaturan',
'avatar.localCli': 'CLI lokal',
+3
View File
@@ -170,6 +170,8 @@ export const ja: Dict = {
'promptTemplates.emptyVideo': '動画プロンプトテンプレートがまだインストールされていません。',
'promptTemplates.emptyNoMatch': '検索に一致するテンプレートがありません。',
'promptTemplates.attributionFooter': '公開プロンプトライブラリから引用。各カードは元の作者にリンクしています。',
'promptTemplates.dsNarrowedHint': '「{title}」に関連するテンプレートを表示しています。',
'promptTemplates.showAllTemplates': 'すべてのテンプレートを表示',
'promptTemplates.openPreviewTitle': 'プロンプトとプレビューを開く',
'promptTemplates.sourcePrefix': 'ソース:',
'promptTemplates.fetchError': 'テンプレートの本文を読み込めませんでした。',
@@ -352,6 +354,7 @@ export const ja: Dict = {
'ds.tokens': 'トークン',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'DESIGN.md を読み込み中…',
'ds.selectForProject': '新規プロジェクトで使う',
'avatar.title': 'アカウントと設定',
'avatar.localCli': 'ローカル CLI',
+3
View File
@@ -171,6 +171,8 @@ export const ko: Dict = {
'promptTemplates.emptyVideo': '설치된 비디오 프롬프트 템플릿이 없습니다.',
'promptTemplates.emptyNoMatch': '검색어와 일치하는 템플릿이 없습니다.',
'promptTemplates.attributionFooter': '공개 프롬프트 라이브러리를 기반으로 합니다. 각 카드는 원작자 페이지로 연결됩니다.',
'promptTemplates.dsNarrowedHint': '「{title}」과 관련된 템플릿만 표시합니다.',
'promptTemplates.showAllTemplates': '모든 템플릿 표시',
'promptTemplates.openPreviewTitle': '프롬프트 열기 및 미리보기',
'promptTemplates.sourcePrefix': '출처:',
'promptTemplates.fetchError': '이 템플릿의 본문을 불러올 수 없습니다.',
@@ -399,6 +401,7 @@ export const ko: Dict = {
'ds.tokens': '토큰',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'DESIGN.md 불러오는 중…',
'ds.selectForProject': '새 프로젝트에 사용',
'avatar.title': '계정 및 설정',
'avatar.localCli': '로컬 CLI',
+3
View File
@@ -171,6 +171,8 @@ export const pl: Dict = {
'promptTemplates.emptyVideo': 'Nie zainstalowano jeszcze szablonów promptów wideo.',
'promptTemplates.emptyNoMatch': 'Brak szablonów pasujących do wyszukiwania.',
'promptTemplates.attributionFooter': 'Zaadaptowano z publicznych bibliotek promptów. Każda karta linkuje do oryginalnego autora.',
'promptTemplates.dsNarrowedHint': 'Wyświetlane są szablony powiązane z „{title}”.',
'promptTemplates.showAllTemplates': 'Pokaż wszystkie szablony',
'promptTemplates.openPreviewTitle': 'Otwórz prompt i podgląd',
'promptTemplates.sourcePrefix': 'Źródło:',
'promptTemplates.fetchError': 'Nie udało się załadować treści szablonu.',
@@ -399,6 +401,7 @@ export const pl: Dict = {
'ds.tokens': 'Tokeny',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'Ładowanie DESIGN.md…',
'ds.selectForProject': 'Użyj w nowym projekcie',
'avatar.title': 'Konto i ustawienia',
'avatar.localCli': 'Lokalne CLI',
+3
View File
@@ -168,6 +168,8 @@ export const ptBR: Dict = {
'promptTemplates.emptyVideo': 'Nenhum template de prompt de vídeo instalado.',
'promptTemplates.emptyNoMatch': 'Nenhum template corresponde à busca.',
'promptTemplates.attributionFooter': 'Adaptado de bibliotecas públicas de prompts. Cada card aponta para o autor original.',
'promptTemplates.dsNarrowedHint': 'Mostrando modelos relacionados a «{title}».',
'promptTemplates.showAllTemplates': 'Mostrar todos os modelos',
'promptTemplates.openPreviewTitle': 'Abrir prompt e prévia',
'promptTemplates.sourcePrefix': 'Fonte:',
'promptTemplates.fetchError': 'Não foi possível carregar o corpo deste template.',
@@ -409,6 +411,7 @@ export const ptBR: Dict = {
'ds.tokens': 'Tokens',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'Carregando DESIGN.md…',
'ds.selectForProject': 'Usar em novo projeto',
'avatar.title': 'Conta e configurações',
'avatar.localCli': 'CLI local',
+3
View File
@@ -168,6 +168,8 @@ export const ru: Dict = {
'promptTemplates.emptyVideo': 'Шаблоны промптов видео не установлены.',
'promptTemplates.emptyNoMatch': 'Нет шаблонов, соответствующих поиску.',
'promptTemplates.attributionFooter': 'Адаптировано из публичных библиотек промптов. Каждая карточка ссылается на исходного автора.',
'promptTemplates.dsNarrowedHint': 'Показаны шаблоны, связанные с «{title}».',
'promptTemplates.showAllTemplates': 'Показать все шаблоны',
'promptTemplates.openPreviewTitle': 'Открыть промпт и предпросмотр',
'promptTemplates.sourcePrefix': 'Источник:',
'promptTemplates.fetchError': 'Не удалось загрузить текст шаблона.',
@@ -409,6 +411,7 @@ export const ru: Dict = {
'ds.tokens': 'Токены',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'Загрузка DESIGN.md…',
'ds.selectForProject': 'Использовать в новом проекте',
'avatar.title': 'Аккаунт и настройки',
'avatar.localCli': 'Локальный CLI',
+3
View File
@@ -165,6 +165,8 @@ export const tr: Dict = {
'promptTemplates.emptyVideo': 'Henüz video istemi şablonu yüklenmedi.',
'promptTemplates.emptyNoMatch': 'Aramanıza uygun şablon bulunamadı.',
'promptTemplates.attributionFooter': 'Herkese açık istem kütüphanesinden adape edildi. Her kart orijinal yazara bağlantılıdır.',
'promptTemplates.dsNarrowedHint': '“{title}” ile ilişkili şablonlar gösteriliyor.',
'promptTemplates.showAllTemplates': 'Tüm şablonları göster',
'promptTemplates.openPreviewTitle': 'istemi aç ve önizle',
'promptTemplates.sourcePrefix': 'Kaynak:',
'promptTemplates.fetchError': 'Bu şablon gövdesi yüklenemedi.',
@@ -392,6 +394,7 @@ export const tr: Dict = {
'ds.tokens': 'Tokenler',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'DESIGN.md yükleniyor…',
'ds.selectForProject': 'Yeni projede kullan',
'avatar.title': 'Hesap & ayarlar',
'avatar.localCli': 'Yerel CLI',
+3
View File
@@ -172,6 +172,8 @@ export const uk: Dict = {
'promptTemplates.emptyVideo': 'Шаблонів підказок для відео ще не встановлено.',
'promptTemplates.emptyNoMatch': 'Немає шаблонів, що відповідають вашому пошуку.',
'promptTemplates.attributionFooter': 'Адаптовано з публічних бібліотек підказок. Кожна картка посилається на автора.',
'promptTemplates.dsNarrowedHint': 'Показано шаблони, пов’язані з «{title}».',
'promptTemplates.showAllTemplates': 'Усі шаблони',
'promptTemplates.openPreviewTitle': 'Відкрити підказку та попередній перегляд',
'promptTemplates.sourcePrefix': 'Джерело:',
'promptTemplates.fetchError': 'Не вдалося завантажити текст цього шаблону.',
@@ -410,6 +412,7 @@ export const uk: Dict = {
'ds.tokens': 'Токени',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': 'Завантаження DESIGN.md…',
'ds.selectForProject': 'Використати в новому проєкті',
'avatar.title': 'Обліковий запис та налаштування',
'avatar.localCli': 'Локальний CLI',
+3
View File
@@ -167,6 +167,8 @@ export const zhCN: Dict = {
'promptTemplates.emptyVideo': '还没有安装视频 Prompt 模板。',
'promptTemplates.emptyNoMatch': '没有匹配的模板。',
'promptTemplates.attributionFooter': '改编自公开 Prompt 库,每张卡片都链接到原作者。',
'promptTemplates.dsNarrowedHint': '正在显示与「{title}」相关的模板。',
'promptTemplates.showAllTemplates': '显示全部模板',
'promptTemplates.openPreviewTitle': '打开 Prompt 与预览',
'promptTemplates.sourcePrefix': '来源:',
'promptTemplates.fetchError': '无法加载此模板正文。',
@@ -404,6 +406,7 @@ export const zhCN: Dict = {
'ds.tokens': 'Token',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': '正在加载 DESIGN.md…',
'ds.selectForProject': '用于新项目',
'avatar.title': '账户与设置',
'avatar.localCli': '本机 CLI',
+3
View File
@@ -167,6 +167,8 @@ export const zhTW: Dict = {
'promptTemplates.emptyVideo': '還沒有安裝影片 Prompt 範本。',
'promptTemplates.emptyNoMatch': '沒有符合的範本。',
'promptTemplates.attributionFooter': '改編自公開 Prompt 庫,每張卡片都連結到原作者。',
'promptTemplates.dsNarrowedHint': '正在顯示與「{title}」相關的模板。',
'promptTemplates.showAllTemplates': '顯示全部模板',
'promptTemplates.openPreviewTitle': '開啟 Prompt 與預覽',
'promptTemplates.sourcePrefix': '來源:',
'promptTemplates.fetchError': '無法載入此範本文字。',
@@ -404,6 +406,7 @@ export const zhTW: Dict = {
'ds.tokens': 'Token',
'ds.specToggle': 'DESIGN.md',
'ds.specLoading': '正在載入 DESIGN.md…',
'ds.selectForProject': '用於新專案',
'avatar.title': '帳號與設定',
'avatar.localCli': '本機 CLI',
+3
View File
@@ -358,6 +358,8 @@ export interface Dict {
'promptTemplates.emptyVideo': string;
'promptTemplates.emptyNoMatch': string;
'promptTemplates.attributionFooter': string;
'promptTemplates.dsNarrowedHint': string;
'promptTemplates.showAllTemplates': string;
'promptTemplates.openPreviewTitle': string;
'promptTemplates.sourcePrefix': string;
'promptTemplates.fetchError': string;
@@ -472,6 +474,7 @@ export interface Dict {
'ds.tokens': string;
'ds.specToggle': string;
'ds.specLoading': string;
'ds.selectForProject': string;
// Avatar menu (project topbar)
'avatar.title': string;
+55 -1
View File
@@ -4324,7 +4324,7 @@ button.connector-action.is-loading {
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
cursor: default;
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.ds-card:hover {
@@ -4347,6 +4347,7 @@ button.connector-action.is-loading {
background: var(--bg-subtle);
overflow: hidden;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.ds-card-thumb iframe {
width: 200%;
@@ -4467,6 +4468,31 @@ button.connector-action.is-loading {
.ds-card-swatches > span + span {
border-left: 1px solid rgba(0, 0, 0, 0.05);
}
.ds-card-actions {
display: flex;
justify-content: stretch;
margin-top: 4px;
}
.ds-card-select-btn {
flex: 1;
font-size: 12px;
font-weight: 600;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg-subtle);
color: var(--text);
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
.ds-card-select-btn:hover {
border-color: var(--accent);
background: var(--accent-tint);
color: var(--accent);
}
.ds-card.active .ds-card-select-btn {
border-color: var(--accent);
}
/* Legacy list classes kept for any consumer outside the tab — the gallery
itself no longer renders these. Safe to remove once nothing references
@@ -8735,6 +8761,34 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
Prompt template gallery
============================================================ */
.prompt-templates-panel { display: flex; flex-direction: column; gap: 16px; }
.prompt-templates-ds-hint {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 14px;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg-subtle);
font-size: 12px;
color: var(--text-muted);
line-height: 1.4;
}
.prompt-templates-ds-hint-clear {
margin-left: auto;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--bg-panel);
color: var(--text);
cursor: pointer;
}
.prompt-templates-ds-hint-clear:hover {
border-color: var(--accent);
color: var(--accent);
}
.prompt-templates-count {
margin-left: auto;
color: var(--text-muted);
@@ -0,0 +1,68 @@
import type { DesignSystemSummary } from '../types';
/**
* Maps a design system's authored metadata to prompt-template gallery categories.
* Used when the workspace default design system is image/video-aligned so the
* template tabs can narrow results without per-template schema coupling.
*/
export function inferPromptTemplateCategoriesForDs(
ds: DesignSystemSummary,
): string[] | null {
const blob = `${ds.category} ${ds.title} ${ds.summary}`.toLowerCase();
const out = new Set<string>();
const add = (cats: string[]) => {
for (const c of cats) out.add(c);
};
if (/anime|manga|illustration|creative|artistic|editorial/i.test(blob)) {
add([
'Anime',
'Anime / Manga',
'Illustration',
'Profile / Avatar',
'Social Media Post',
]);
}
if (/game|gaming|\bgui\b|\bui\b|interface/i.test(blob)) {
add(['Game UI', 'App / Web Design']);
}
if (/e-?commerce|retail|shopping|product|saas|marketplace|store/i.test(blob)) {
add(['Product', 'Social Media Post', 'Marketing', 'App / Web Design']);
}
if (/fintech|finance|crypto|payment|bank|stripe/i.test(blob)) {
add(['App / Web Design', 'Data', 'Marketing', 'Branding']);
}
if (/developer|tool|api|backend|data|engineering|llm|ai\b/i.test(blob)) {
add(['App / Web Design', 'Data', 'General']);
}
if (
/video|cinematic|film|motion|advertis|marketing|media|social|meme|travel|vfx|fantasy|short form/i.test(
blob,
)
) {
add([
'Cinematic',
'Motion Graphics',
'Advertising',
'Marketing',
'Social / Meme',
'Travel',
'VFX / Fantasy',
'Short Form',
]);
}
if (/automotive|car|vehicle|motor/i.test(blob)) {
add(['Product', 'Cinematic', 'Advertising']);
}
if (/\bbrand/i.test(blob)) {
add(['Branding']);
}
if (/infographic|data\s+viz|chart|diagram/i.test(blob)) {
add(['Infographic', 'Data']);
}
if (/profile|avatar|portrait/i.test(blob)) {
add(['Profile / Avatar']);
}
return out.size > 0 ? [...out] : null;
}