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);
+ });
+});