fix(web): show published design systems as "Published" on project cards (#2849)

A published design-system project still showed its last generation run status on its card. When that run had failed, the published system read as "Failed" on the home Recent projects strip and in the Projects grid. The system is published and live, so showing a stale run failure there is wrong.

Cards now read "Published" when the backing design system's status is published, keyed off the design system summary instead of the project run status. Every other project keeps its run status.

A shared isPublishedDesignSystemProject helper lets the home strip and the Designs grid apply one rule. The label uses a new designs.status.published key in the locale files, with a green style that matches the existing succeeded color.
This commit is contained in:
Chris Seifert 2026-05-24 23:14:26 -04:00 committed by GitHub
parent af997b7cf5
commit 085963a1e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 130 additions and 13 deletions

View file

@ -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 (
<div
key={p.id}
@ -798,9 +800,9 @@ export function DesignsTab({
{skill ? ` · ${skill}` : ""}
{" · "}
<span
className={`design-card-status design-card-status-${status}`}
className={`design-card-status design-card-status-${publishedDesignSystem ? "published" : status}`}
>
{statusLabel(status, t)}
{publishedDesignSystem ? t("designs.status.published") : statusLabel(status, t)}
</span>
</span>
{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,

View file

@ -1326,6 +1326,7 @@ export function HomeView({
<RecentProjectsStrip
projects={projects}
designSystems={designSystems}
{...(projectsLoading !== undefined ? { loading: projectsLoading } : {})}
onOpen={(id) => {
// P0 ui_click area=recent_projects element=project_card — emit

View file

@ -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<string, string>();
@ -31,6 +37,7 @@ const deckCoverInflight = new Map<string, Promise<string>>();
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 (
<button
key={project.id}
@ -194,12 +203,12 @@ export function RecentProjectsStrip({
<div className="recent-projects__card-name">{project.name}</div>
<div className="recent-projects__card-time">
<span
className={`recent-projects__card-status recent-projects__card-status-${status}`}
className={`recent-projects__card-status recent-projects__card-status-${publishedDesignSystem ? 'published' : status}`}
>
{isActive ? (
<span className="recent-projects__card-status-dot" aria-hidden />
) : null}
{statusLabel(status, t)}
{publishedDesignSystem ? t('designs.status.published') : statusLabel(status, t)}
</span>
<span className="recent-projects__card-sep" aria-hidden>·</span>
{relativeTime(project.updatedAt, t)}
@ -465,10 +474,6 @@ function ProjectTag({ category }: { category: ProjectCategory }) {
return <span className={`design-card-tag tag-${category}`}>{label}</span>;
}
function isDesignSystemProject(project: Project): boolean {
return project.metadata?.importedFrom === 'design-system';
}
function DesignSystemProjectTag() {
return <span className="design-card-tag tag-design-system">Design System</span>;
}

View file

@ -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',
);
}

View file

@ -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': 'وضع العرض',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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': 'حالت نمایش',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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': '表示モード',

View file

@ -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': '보기 모드',

View file

@ -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',

View file

@ -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',

View file

@ -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': 'Режим просмотра',

View file

@ -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': 'โหมดมุมมอง',

View file

@ -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',

View file

@ -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': 'Режим перегляду',

View file

@ -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': '视图模式',

View file

@ -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': '檢視模式',

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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> = {}): Project {
return {
id: 'p1',
name: 'StackMe',
updatedAt: 1,
designSystemId: null,
...overrides,
} as Project;
}
function system(overrides: Partial<DesignSystemSummary> = {}): 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);
});
});