diff --git a/apps/web/src/components/DesignsTab.tsx b/apps/web/src/components/DesignsTab.tsx index 179a6ae01..9de1cb67e 100644 --- a/apps/web/src/components/DesignsTab.tsx +++ b/apps/web/src/components/DesignsTab.tsx @@ -19,6 +19,7 @@ import type { SkillSummary, } from "../types"; import { Icon } from "./Icon"; +import { isDesignSystemProject, isPublishedDesignSystemProject } from "./design-system-project"; import { LiveArtifactBadges } from "./LiveArtifactBadges"; import { Toast } from "./Toast"; @@ -633,6 +634,7 @@ export function DesignsTab({ const cover = projectCover(p, coverByProject[p.id] ?? null); const isSelected = selected.has(p.id); const designSystemProject = isDesignSystemProject(p); + const publishedDesignSystem = isPublishedDesignSystemProject(p, designSystems); return (
- {statusLabel(status, t)} + {publishedDesignSystem ? t("designs.status.published") : statusLabel(status, t)} {sub === "recent" || sub === "yours" ? ( @@ -1044,9 +1046,6 @@ function isOrbitProject(project: Project): boolean { return metadata?.kind === 'orbit'; } -function isDesignSystemProject(project: Project): boolean { - return project.metadata?.importedFrom === "design-system"; -} function projectCover( project: Project, diff --git a/apps/web/src/components/HomeView.tsx b/apps/web/src/components/HomeView.tsx index 4f274f807..f0969ac2f 100644 --- a/apps/web/src/components/HomeView.tsx +++ b/apps/web/src/components/HomeView.tsx @@ -1326,6 +1326,7 @@ export function HomeView({ { // P0 ui_click area=recent_projects element=project_card — emit diff --git a/apps/web/src/components/RecentProjectsStrip.tsx b/apps/web/src/components/RecentProjectsStrip.tsx index 160149e6d..30438b42b 100644 --- a/apps/web/src/components/RecentProjectsStrip.tsx +++ b/apps/web/src/components/RecentProjectsStrip.tsx @@ -10,12 +10,16 @@ import type { CSSProperties } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useT } from '../i18n'; import { fetchProjectFiles, projectFileUrl } from '../providers/registry'; -import type { Project, ProjectDisplayStatus, ProjectFile } from '../types'; +import type { DesignSystemSummary, Project, ProjectDisplayStatus, ProjectFile } from '../types'; import { Icon } from './Icon'; import { STATUS_LABEL_KEYS } from './DesignsTab'; +import { isDesignSystemProject, isPublishedDesignSystemProject } from './design-system-project'; interface Props { projects: Project[]; + /** Used only to show a "Published" status for design-system projects whose + * backing system is published (independent of the project's run status). */ + designSystems?: DesignSystemSummary[]; /** Retained for call-site compatibility; the strip skips rendering * while the list is loading so we never need a loading state. */ loading?: boolean; @@ -24,6 +28,8 @@ interface Props { limit?: number; } +const EMPTY_DESIGN_SYSTEMS: DesignSystemSummary[] = []; + const DECK_PREVIEW_WIDTH = 1280; const DECK_PREVIEW_HEIGHT = 720; const deckCoverCache = new Map(); @@ -31,6 +37,7 @@ const deckCoverInflight = new Map>(); export function RecentProjectsStrip({ projects, + designSystems = EMPTY_DESIGN_SYSTEMS, onOpen, onViewAll, limit = 6, @@ -142,8 +149,10 @@ export function RecentProjectsStrip({ const cover = projectCover(project, coverByProject[project.id] ?? null); const designSystemProject = isDesignSystemProject(project); const status: ProjectDisplayStatus = project.status?.value ?? 'not_started'; + const publishedDesignSystem = isPublishedDesignSystemProject(project, designSystems); const isActive = - status === 'running' || status === 'queued' || status === 'awaiting_input'; + !publishedDesignSystem && + (status === 'running' || status === 'queued' || status === 'awaiting_input'); return (
{isActive ? ( ) : null} - {statusLabel(status, t)} + {publishedDesignSystem ? t('designs.status.published') : statusLabel(status, t)} · {relativeTime(project.updatedAt, t)} @@ -465,10 +474,6 @@ function ProjectTag({ category }: { category: ProjectCategory }) { return {label}; } -function isDesignSystemProject(project: Project): boolean { - return project.metadata?.importedFrom === 'design-system'; -} - function DesignSystemProjectTag() { return Design System; } diff --git a/apps/web/src/components/design-system-project.ts b/apps/web/src/components/design-system-project.ts new file mode 100644 index 000000000..e1a55ec5f --- /dev/null +++ b/apps/web/src/components/design-system-project.ts @@ -0,0 +1,22 @@ +import type { DesignSystemSummary, Project } from '../types'; + +/** A project imported from / backing a design system. */ +export function isDesignSystemProject(project: Project): boolean { + return project.metadata?.importedFrom === 'design-system'; +} + +/** + * True when a project is a design system whose backing system is published. + * The publish state lives on the DesignSystemSummary (keyed by designSystemId), + * not on the project's run status, so a published system whose last generation + * run failed should still read as published in project cards. + */ +export function isPublishedDesignSystemProject( + project: Project, + designSystems: readonly DesignSystemSummary[], +): boolean { + if (!isDesignSystemProject(project) || !project.designSystemId) return false; + return designSystems.some( + (system) => system.id === project.designSystemId && system.status === 'published', + ); +} diff --git a/apps/web/src/i18n/locales/ar.ts b/apps/web/src/i18n/locales/ar.ts index de6f6ad1f..d7e4a82de 100644 --- a/apps/web/src/i18n/locales/ar.ts +++ b/apps/web/src/i18n/locales/ar.ts @@ -596,6 +596,7 @@ export const ar: Dict = { 'designs.status.running': 'جاري التشغيل', 'designs.status.awaitingInput': 'يحتاج مدخلات', 'designs.status.succeeded': 'اكتمل', + 'designs.status.published': 'تم النشر', 'designs.status.failed': 'فشل', 'designs.status.canceled': 'ملغى', 'designs.viewToggleAria': 'وضع العرض', diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index dcd3f9146..6ec997df9 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -484,6 +484,7 @@ export const de: Dict = { 'designs.status.running': 'Läuft', 'designs.status.awaitingInput': 'Eingabe nötig', 'designs.status.succeeded': 'Abgeschlossen', + 'designs.status.published': 'Veröffentlicht', 'designs.status.failed': 'Fehlgeschlagen', 'designs.status.canceled': 'Abgebrochen', 'designs.viewToggleAria': 'Ansichtsmodus', diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index 8ede25760..d2d2e0ac4 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -1180,6 +1180,7 @@ export const en: Dict = { 'designs.status.running': 'Running', 'designs.status.awaitingInput': 'Needs input', 'designs.status.succeeded': 'Completed', + 'designs.status.published': 'Published', 'designs.status.failed': 'Failed', 'designs.status.canceled': 'Canceled', 'designs.viewToggleAria': 'View mode', diff --git a/apps/web/src/i18n/locales/es-ES.ts b/apps/web/src/i18n/locales/es-ES.ts index 9841dbeda..bc3bc92be 100644 --- a/apps/web/src/i18n/locales/es-ES.ts +++ b/apps/web/src/i18n/locales/es-ES.ts @@ -485,6 +485,7 @@ export const esES: Dict = { 'designs.status.running': 'En ejecución', 'designs.status.awaitingInput': 'Necesita respuesta', 'designs.status.succeeded': 'Completado', + 'designs.status.published': 'Publicado', 'designs.status.failed': 'Fallido', 'designs.status.canceled': 'Cancelado', 'designs.viewToggleAria': 'Modo de vista', diff --git a/apps/web/src/i18n/locales/fa.ts b/apps/web/src/i18n/locales/fa.ts index abb88dd3f..3e241f56e 100644 --- a/apps/web/src/i18n/locales/fa.ts +++ b/apps/web/src/i18n/locales/fa.ts @@ -618,6 +618,7 @@ export const fa: Dict = { 'designs.status.running': 'در حال اجرا', 'designs.status.awaitingInput': 'نیازمند ورودی', 'designs.status.succeeded': 'تکمیل شد', + 'designs.status.published': 'منتشر شد', 'designs.status.failed': 'ناموفق', 'designs.status.canceled': 'لغو شد', 'designs.viewToggleAria': 'حالت نمایش', diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index e33bea448..b5e265b37 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -612,6 +612,7 @@ export const fr: Dict = { 'designs.status.running': 'En cours', 'designs.status.awaitingInput': 'Entrée requise', 'designs.status.succeeded': 'Terminé', + 'designs.status.published': 'Publié', 'designs.status.failed': 'Échoué', 'designs.status.canceled': 'Annulé', 'designs.viewToggleAria': 'Mode d\'affichage', diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index a009f1693..9a974203a 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -596,6 +596,7 @@ export const hu: Dict = { 'designs.status.running': 'Fut', 'designs.status.awaitingInput': 'Bevitelre vár', 'designs.status.succeeded': 'Befejezve', + 'designs.status.published': 'Közzétéve', 'designs.status.failed': 'Sikertelen', 'designs.status.canceled': 'Megszakítva', 'designs.viewToggleAria': 'Nézet módja', diff --git a/apps/web/src/i18n/locales/id.ts b/apps/web/src/i18n/locales/id.ts index 863002270..a82e7cf12 100644 --- a/apps/web/src/i18n/locales/id.ts +++ b/apps/web/src/i18n/locales/id.ts @@ -709,6 +709,7 @@ export const id: Dict = { 'designs.status.running': 'Berjalan', 'designs.status.awaitingInput': 'Menunggu input', 'designs.status.succeeded': 'Selesai', + 'designs.status.published': 'Diterbitkan', 'designs.status.failed': 'Gagal', 'designs.status.canceled': 'Dibatalkan', 'designs.viewToggleAria': 'Mode tampilan', diff --git a/apps/web/src/i18n/locales/it.ts b/apps/web/src/i18n/locales/it.ts index ea613a89e..44858aee8 100644 --- a/apps/web/src/i18n/locales/it.ts +++ b/apps/web/src/i18n/locales/it.ts @@ -564,6 +564,7 @@ export const it: Dict = { 'designs.status.running': 'In corso', 'designs.status.awaitingInput': 'Input richiesto', 'designs.status.succeeded': 'Completato', + 'designs.status.published': 'Pubblicato', 'designs.status.failed': 'Fallito', 'designs.status.canceled': 'Annullato', 'designs.viewToggleAria': 'Modalità di visualizzazione', diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index 06931459f..876e1fffd 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -483,6 +483,7 @@ export const ja: Dict = { 'designs.status.running': '実行中', 'designs.status.awaitingInput': '入力待ち', 'designs.status.succeeded': '完了', + 'designs.status.published': '公開済み', 'designs.status.failed': '失敗', 'designs.status.canceled': 'キャンセル済み', 'designs.viewToggleAria': '表示モード', diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index 84a2867b4..01b7390fb 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -596,6 +596,7 @@ export const ko: Dict = { 'designs.status.running': '실행 중', 'designs.status.awaitingInput': '입력 대기 중', 'designs.status.succeeded': '완료됨', + 'designs.status.published': '게시됨', 'designs.status.failed': '실패', 'designs.status.canceled': '취소됨', 'designs.viewToggleAria': '보기 모드', diff --git a/apps/web/src/i18n/locales/pl.ts b/apps/web/src/i18n/locales/pl.ts index a084ba97c..c0194255f 100644 --- a/apps/web/src/i18n/locales/pl.ts +++ b/apps/web/src/i18n/locales/pl.ts @@ -596,6 +596,7 @@ export const pl: Dict = { 'designs.status.running': 'Uruchomiony', 'designs.status.awaitingInput': 'Wymaga danych', 'designs.status.succeeded': 'Zakończono', + 'designs.status.published': 'Opublikowano', 'designs.status.failed': 'Błąd', 'designs.status.canceled': 'Anulowano', 'designs.viewToggleAria': 'Tryb widoku', diff --git a/apps/web/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts index 3326cd3cf..6fb5973c3 100644 --- a/apps/web/src/i18n/locales/pt-BR.ts +++ b/apps/web/src/i18n/locales/pt-BR.ts @@ -617,6 +617,7 @@ export const ptBR: Dict = { 'designs.status.running': 'Em execução', 'designs.status.awaitingInput': 'Aguardando resposta', 'designs.status.succeeded': 'Concluído', + 'designs.status.published': 'Publicado', 'designs.status.failed': 'Falhou', 'designs.status.canceled': 'Cancelado', 'designs.viewToggleAria': 'Modo de visualização', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index 05282302c..60a2348e3 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -617,6 +617,7 @@ export const ru: Dict = { 'designs.status.running': 'Выполняется', 'designs.status.awaitingInput': 'Нужен ввод', 'designs.status.succeeded': 'Завершено', + 'designs.status.published': 'Опубликовано', 'designs.status.failed': 'Ошибка', 'designs.status.canceled': 'Отменено', 'designs.viewToggleAria': 'Режим просмотра', diff --git a/apps/web/src/i18n/locales/th.ts b/apps/web/src/i18n/locales/th.ts index 2b37e30fc..3ec686969 100644 --- a/apps/web/src/i18n/locales/th.ts +++ b/apps/web/src/i18n/locales/th.ts @@ -572,6 +572,7 @@ export const th: Dict = { 'designs.status.running': 'กำลังทำงาน', 'designs.status.awaitingInput': 'ต้องการข้อมูลเพิ่ม', 'designs.status.succeeded': 'สำเร็จ', + 'designs.status.published': 'เผยแพร่แล้ว', 'designs.status.failed': 'ล้มเหลว', 'designs.status.canceled': 'ยกเลิกแล้ว', 'designs.viewToggleAria': 'โหมดมุมมอง', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index 66990b691..d761e0018 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -585,6 +585,7 @@ export const tr: Dict = { 'designs.status.running': 'Çalışıyor', 'designs.status.awaitingInput': 'Girdi gerekli', 'designs.status.succeeded': 'Tamamlandı', + 'designs.status.published': 'Yayımlandı', 'designs.status.failed': 'Başarısız', 'designs.status.canceled': 'İptal edildi', 'designs.viewToggleAria': 'Görüntüleme modu', diff --git a/apps/web/src/i18n/locales/uk.ts b/apps/web/src/i18n/locales/uk.ts index b032615b3..cd931e1fd 100644 --- a/apps/web/src/i18n/locales/uk.ts +++ b/apps/web/src/i18n/locales/uk.ts @@ -609,6 +609,7 @@ export const uk: Dict = { 'designs.status.running': 'Виконується', 'designs.status.awaitingInput': 'Чекає підтвердження', 'designs.status.succeeded': 'Завершено', + 'designs.status.published': 'Опубліковано', 'designs.status.failed': 'Помилка', 'designs.status.canceled': 'Скасовано', 'designs.viewToggleAria': 'Режим перегляду', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index eb341a259..b20ddf494 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -1172,6 +1172,7 @@ export const zhCN: Dict = { 'designs.status.running': '运行中', 'designs.status.awaitingInput': '等待回复', 'designs.status.succeeded': '已完成', + 'designs.status.published': '已发布', 'designs.status.failed': '失败', 'designs.status.canceled': '已取消', 'designs.viewToggleAria': '视图模式', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index a3633fc64..4c33918d1 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -788,6 +788,7 @@ export const zhTW: Dict = { 'designs.status.running': '執行中', 'designs.status.awaitingInput': '等待回覆', 'designs.status.succeeded': '已完成', + 'designs.status.published': '已發佈', 'designs.status.failed': '失敗', 'designs.status.canceled': '已取消', 'designs.viewToggleAria': '檢視模式', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index ffc42a424..668372271 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -1486,6 +1486,7 @@ export interface Dict { 'designs.status.succeeded': string; 'designs.status.failed': string; 'designs.status.canceled': string; + 'designs.status.published': string; 'designs.viewToggleAria': string; 'designs.viewGrid': string; 'designs.viewKanban': string; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 814728228..25318aade 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -9115,6 +9115,9 @@ button.connector-action.is-loading { .design-card-status-failed { color: var(--red); } +.design-card-status-published { + color: var(--green); +} .design-card-close { position: absolute; top: 8px; diff --git a/apps/web/src/styles/home/recent-projects.css b/apps/web/src/styles/home/recent-projects.css index 6f0f719da..2512fb23e 100644 --- a/apps/web/src/styles/home/recent-projects.css +++ b/apps/web/src/styles/home/recent-projects.css @@ -199,6 +199,9 @@ .recent-projects__card-status-failed { color: var(--red); } +.recent-projects__card-status-published { + color: var(--green); +} .recent-projects__card-status-dot { width: 6px; height: 6px; diff --git a/apps/web/tests/components/design-system-project.test.ts b/apps/web/tests/components/design-system-project.test.ts new file mode 100644 index 000000000..55663470d --- /dev/null +++ b/apps/web/tests/components/design-system-project.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest'; +import type { DesignSystemSummary, Project } from '../../src/types'; + +import { + isDesignSystemProject, + isPublishedDesignSystemProject, +} from '../../src/components/design-system-project'; + +function project(overrides: Partial = {}): Project { + return { + id: 'p1', + name: 'StackMe', + updatedAt: 1, + designSystemId: null, + ...overrides, + } as Project; +} + +function system(overrides: Partial = {}): DesignSystemSummary { + return { + id: 'user:stackme', + title: 'StackMe', + category: 'Custom', + summary: '', + swatches: [], + surface: 'web', + source: 'user', + status: 'draft', + isEditable: true, + ...overrides, + }; +} + +describe('isDesignSystemProject', () => { + it('matches projects imported from a design system', () => { + expect(isDesignSystemProject(project({ metadata: { kind: 'other', importedFrom: 'design-system' } }))).toBe(true); + expect(isDesignSystemProject(project({ metadata: { kind: 'prototype' } }))).toBe(false); + expect(isDesignSystemProject(project())).toBe(false); + }); +}); + +describe('isPublishedDesignSystemProject', () => { + const dsProject = project({ + metadata: { kind: 'other', importedFrom: 'design-system' }, + designSystemId: 'user:stackme', + }); + + it('is true when the backing design system is published, even if the run failed', () => { + expect(isPublishedDesignSystemProject(dsProject, [system({ status: 'published' })])).toBe(true); + }); + + it('is false while the design system is still a draft', () => { + expect(isPublishedDesignSystemProject(dsProject, [system({ status: 'draft' })])).toBe(false); + }); + + it('is false for non-design-system projects and when the system is missing', () => { + expect( + isPublishedDesignSystemProject(project({ metadata: { kind: 'prototype' } }), [ + system({ status: 'published' }), + ]), + ).toBe(false); + expect(isPublishedDesignSystemProject(dsProject, [])).toBe(false); + }); +});