import { useEffect, useMemo, useRef, useState } from 'react'; import { useAnalytics } from '../analytics/provider'; import { trackDesignSystemsTemplateCardClick, trackDesignSystemsTopClick, trackDesignSystemStatusResult, trackPageView, } from '../analytics/events'; import type { TrackingDesignSystemStatusAction, TrackingDesignSystemStatusValue, } from '@open-design/contracts/analytics'; import { useI18n } from '../i18n'; import { localizeDesignSystemCategory, localizeDesignSystemSummary, } from '../i18n/content'; import { deleteDesignSystemDraft, fetchDesignSystemShowcase, updateDesignSystemDraft, } from '../providers/registry'; import { buildSrcdoc } from '../runtime/srcdoc'; import { Icon } from './Icon'; import type { DesignSystemSummary, ProjectTemplate, Surface } from '../types'; interface Props { systems: DesignSystemSummary[]; selectedId: string | null; onSelect: (id: string) => void; onPreview: (id: string) => void; onCreate?: () => void; onOpenSystem?: (id: string) => void; onSystemsRefresh?: () => Promise | void; templates?: ProjectTemplate[]; } const CATEGORY_ORDER = [ 'Starter', 'AI & LLM', 'Developer Tools', 'Productivity & SaaS', 'Backend & Data', 'Design & Creative', 'Fintech & Crypto', 'E-Commerce & Retail', 'Media & Consumer', 'Automotive', ]; type SurfaceFilter = 'all' | Surface; type UserListFilter = 'all' | 'published' | 'draft'; type PrimaryCollection = 'design-system' | 'template'; type DesignSystemCollection = 'mine' | 'official' | 'enterprise'; type TemplateCollection = 'mine' | 'enterprise'; const SURFACE_PILLS: { value: SurfaceFilter; labelKey: 'examples.modeAll' | 'ds.surfaceWeb' | 'ds.surfaceImage' | 'ds.surfaceVideo' | 'ds.surfaceAudio' }[] = [ { value: 'all', labelKey: 'examples.modeAll' }, { value: 'web', labelKey: 'ds.surfaceWeb' }, { value: 'image', labelKey: 'ds.surfaceImage' }, { value: 'video', labelKey: 'ds.surfaceVideo' }, { value: 'audio', labelKey: 'ds.surfaceAudio' }, ]; function surfaceOf(system: DesignSystemSummary): Surface { return system.surface ?? 'web'; } function isUserSystem(system: DesignSystemSummary): boolean { return system.source === 'user' || system.isEditable === true; } // `system.status` is the DesignSystemSummary status string from the // daemon; map it onto the tracking enum used by // `design_system_status_result.status_before|status_after`. The // summary type today only carries `'draft' | 'published'`; the wider // tracking enum keeps room for `ready`/`failed`/`archived` once those // land server-side. Unknown values collapse to `'unknown'`. function mapStatusToTracking( status: string | null | undefined, ): TrackingDesignSystemStatusValue { switch (status) { case 'draft': case 'published': return status; default: return 'unknown'; } } function formatShortDate( value: number | string | undefined, locale?: string, emptyLabel = 'just now', ): string { if (!value) return emptyLabel; const time = typeof value === 'number' ? value : Date.parse(value); if (!Number.isFinite(time)) return String(value); return new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }).format(new Date(time)); } export function DesignSystemsTab({ systems, selectedId, onSelect, onPreview, onCreate, onOpenSystem, onSystemsRefresh, templates = [], }: Props) { const { locale, t } = useI18n(); const isZhCN = locale === 'zh-CN'; const primaryDesignSystemLabel = isZhCN ? t('ds.userSystemsEyebrow') : 'Design system'; const primaryTemplateLabel = isZhCN ? t('ds.templatesTitle') : 'Template'; const userSystemsLabel = isZhCN ? t('ds.userSystemsTitle') : 'Your systems'; const officialPresetsLabel = isZhCN ? t('ds.libraryTitle') : 'Official presets'; const enterpriseLabel = isZhCN ? '企业' : 'Enterprise'; const templateMineLabel = isZhCN ? t('ds.templatesTitle') : 'Your templates'; const analytics = useAnalytics(); const designSystemsPageViewFiredRef = useRef(false); useEffect(() => { if (designSystemsPageViewFiredRef.current) return; designSystemsPageViewFiredRef.current = true; // v2 doc: the DS list page also carries `area` / `view_type` / // `entry_from` so it can stitch the cross-surface DS funnel. // `entry_from` is `unknown` here because the tab is reached // through the home nav rail; a router-aware entry mapper can // refine this later. trackPageView(analytics.track, { page_name: 'design_systems', area: 'design_system_list', view_type: 'page', entry_from: 'unknown', available_design_system_count: systems.length, }); }, [analytics.track, systems.length]); const searchTrackedRef = useRef(false); const categoryTrackedRef = useRef(false); const [filter, setFilter] = useState(''); const [userFilter, setUserFilter] = useState('all'); const [busyId, setBusyId] = useState(null); const [primaryCollection, setPrimaryCollection] = useState('design-system'); const [designSystemCollection, setDesignSystemCollection] = useState('mine'); const [templateCollection, setTemplateCollection] = useState('mine'); const [surfaceFilter, setSurfaceFilter] = useState('all'); const [category, setCategory] = useState('All'); // Cache fetched showcase HTML across re-renders so cards never re-flicker // when the user filters / scrolls back. null = "in flight"; undefined = // "not yet requested". Mirrors the pattern used by ExamplesTab. const [thumbs, setThumbs] = useState>({}); const librarySystems = useMemo( () => systems.filter((system) => !isUserSystem(system)), [systems], ); const surfaceScoped = useMemo( () => surfaceFilter === 'all' ? librarySystems : librarySystems.filter((s) => surfaceOf(s) === surfaceFilter), [librarySystems, surfaceFilter], ); const userSystems = useMemo(() => { const editable = systems.filter(isUserSystem); if (userFilter === 'all') return editable; return editable.filter((system) => (system.status ?? 'draft') === userFilter); }, [systems, userFilter]); const showOfficialLibrary = primaryCollection === 'design-system' && (designSystemCollection === 'official' || (designSystemCollection === 'mine' && userSystems.length === 0 && librarySystems.length > 0)); // Total systems per surface, ignoring every active filter. Drives the // "this surface is now empty" fallback below — that guard must react to // the catalog itself, not to a transient style/search filter. const surfaceTotals = useMemo(() => { const counts: Record = { all: librarySystems.length, web: 0, image: 0, video: 0, audio: 0 }; for (const s of librarySystems) counts[surfaceOf(s)]++; return counts; }, [librarySystems]); const categories = useMemo(() => { const cats = new Set(); for (const s of surfaceScoped) cats.add(s.category || 'Uncategorized'); const ordered: string[] = []; for (const c of CATEGORY_ORDER) if (cats.has(c)) ordered.push(c); for (const c of [...cats].sort()) if (!ordered.includes(c)) ordered.push(c); return ['All', ...ordered]; }, [surfaceScoped]); // Keep surfaceFilter and category in sync when systems changes dynamically. // If the currently selected surface has zero items, fall back to 'all'. // If the current category is no longer present in the filtered list, fall back to 'All'. useEffect(() => { if (surfaceFilter !== 'all' && surfaceTotals[surfaceFilter] === 0) { setSurfaceFilter('all'); setCategory('All'); } else if (category !== 'All' && !categories.includes(category)) { setCategory('All'); } }, [systems, surfaceFilter, surfaceTotals, category, categories]); // Systems matching the active style category and search text, before the // surface filter is applied. Both the surface pill counts and the visible // grid derive from this so a surface chip always reports its own result // set rather than the unfiltered catalog total. const queryScoped = useMemo(() => { const q = filter.trim().toLowerCase(); return librarySystems.filter((s) => { if (category !== 'All' && (s.category || 'Uncategorized') !== category) return false; if (!q) return true; const summary = localizeDesignSystemSummary(locale, s).toLowerCase(); const categoryLabel = localizeDesignSystemCategory( locale, s.category || 'Uncategorized', ).toLowerCase(); return ( s.title.toLowerCase().includes(q) || s.summary.toLowerCase().includes(q) || summary.includes(q) || categoryLabel.includes(q) ); }); }, [librarySystems, filter, category, locale]); const surfaceCounts = useMemo(() => { const counts: Record = { all: queryScoped.length, web: 0, image: 0, video: 0, audio: 0, }; for (const s of queryScoped) counts[surfaceOf(s)]++; return counts; }, [queryScoped]); const filtered = useMemo( () => surfaceFilter === 'all' ? queryScoped : queryScoped.filter((s) => surfaceOf(s) === surfaceFilter), [queryScoped, surfaceFilter], ); // Category metadata is authored in English; keep raw values in state for // filtering while localizing the visible labels for the current UI locale. const renderCategory = (c: string) => { if (c === 'All') return t('ds.categoryAll'); if (c === 'Uncategorized') return t('ds.categoryUncategorized'); return localizeDesignSystemCategory(locale, c); }; function loadThumb(id: string) { setThumbs((prev) => { if (prev[id] !== undefined) return prev; void fetchDesignSystemShowcase(id).then((html) => { setThumbs((p) => ({ ...p, [id]: html })); }); return { ...prev, [id]: null }; }); } async function refreshSystems() { await onSystemsRefresh?.(); } async function togglePublished(system: DesignSystemSummary) { setBusyId(system.id); const startedAt = performance.now(); const willPublish = system.status !== 'published'; const action: TrackingDesignSystemStatusAction = willPublish ? 'publish' : 'unpublish'; const statusBefore = mapStatusToTracking(system.status); const isDefaultBefore = system.id === selectedId; let succeeded = false; let errorCode: string | undefined; try { const updated = await updateDesignSystemDraft(system.id, { status: willPublish ? 'published' : 'draft', }); succeeded = Boolean(updated); if (!succeeded) errorCode = 'DS_STATUS_UPDATE_RETURNED_NULL'; await refreshSystems(); } catch (err) { errorCode = err instanceof Error ? `DS_STATUS_UPDATE_THREW:${err.message.slice(0, 80)}` : 'DS_STATUS_UPDATE_THREW'; throw err; } finally { setBusyId(null); trackDesignSystemStatusResult(analytics.track, { page_name: 'design_systems', area: 'design_system_status', action, result: succeeded ? 'success' : 'failed', design_system_id: system.id, status_before: statusBefore, status_after: succeeded ? willPublish ? 'published' : 'draft' : statusBefore, is_default_before: isDefaultBefore, is_default_after: isDefaultBefore, error_code: errorCode, duration_ms: Math.round(performance.now() - startedAt), }); } } async function deleteSystem(system: DesignSystemSummary) { const ok = window.confirm(t('ds.deleteConfirm', { title: system.title })); if (!ok) { trackDesignSystemStatusResult(analytics.track, { page_name: 'design_systems', area: 'design_system_status', action: 'delete', result: 'cancelled', design_system_id: system.id, status_before: mapStatusToTracking(system.status), status_after: mapStatusToTracking(system.status), is_default_before: system.id === selectedId, is_default_after: system.id === selectedId, duration_ms: 0, }); return; } setBusyId(system.id); const startedAt = performance.now(); const statusBefore = mapStatusToTracking(system.status); const wasDefault = system.id === selectedId; let succeeded = false; let errorCode: string | undefined; try { const deleted = await deleteDesignSystemDraft(system.id); succeeded = Boolean(deleted); if (!succeeded) errorCode = 'DS_DELETE_RETURNED_FALSE'; if (succeeded && selectedId === system.id) { const fallback = systems.find((candidate) => candidate.id !== system.id && isUserSystem(candidate), ); if (fallback) onSelect(fallback.id); } await refreshSystems(); } catch (err) { errorCode = err instanceof Error ? `DS_DELETE_THREW:${err.message.slice(0, 80)}` : 'DS_DELETE_THREW'; throw err; } finally { setBusyId(null); trackDesignSystemStatusResult(analytics.track, { page_name: 'design_systems', area: 'design_system_status', action: 'delete', result: succeeded ? 'success' : 'failed', design_system_id: system.id, status_before: statusBefore, status_after: succeeded ? 'deleted' : statusBefore, is_default_before: wasDefault, // After a successful delete the row is gone; if it was the // default the consumer remapped to a fallback above, so this // DS is no longer the default either way. is_default_after: false, error_code: errorCode, duration_ms: Math.round(performance.now() - startedAt), }); } } function handleMakeDefaultClick(system: DesignSystemSummary): void { const wasDefault = system.id === selectedId; const statusBefore = mapStatusToTracking(system.status); onSelect(system.id); trackDesignSystemStatusResult(analytics.track, { page_name: 'design_systems', area: 'design_system_status', action: wasDefault ? 'unset_default' : 'set_default', result: 'success', design_system_id: system.id, status_before: statusBefore, status_after: statusBefore, is_default_before: wasDefault, is_default_after: !wasDefault, duration_ms: 0, }); } return (
{primaryCollection === 'design-system' ? (
) : (
)} {primaryCollection === 'design-system' ? (

{t('ds.privateNote')}

) : null} {primaryCollection === 'design-system' && designSystemCollection === 'mine' ? (
{t('ds.userSystemsEyebrow')}

{t('ds.userSystemsTitle')}

{onCreate ? ( ) : null} {userSystems.length === 0 ? (
{t('ds.userEmpty')}
) : (
{userSystems.map((system) => { const status = system.status ?? 'draft'; const canUseInProjects = status === 'published'; const selected = canUseInProjects && system.id === selectedId; const busy = busyId === system.id; return (
{onOpenSystem ? ( ) : null} {!selected && canUseInProjects ? ( ) : null} {onOpenSystem ? ( ) : null}
); })}
)}
) : null} {showOfficialLibrary ? (
{t('ds.libraryEyebrow')}

{t('ds.libraryTitle')}

{ if (searchTrackedRef.current) return; searchTrackedRef.current = true; trackDesignSystemsTopClick(analytics.track, { page_name: 'design_systems', area: 'design_systems', element: 'search_input', }); }} onChange={(e) => setFilter(e.target.value)} />
{t('ds.surfaceLabel')} {/* Hide chips with no items in the active style/search filter, but always keep "all" and the currently selected surface — otherwise a transient search could remove the active chip and leave the grid filtered with no chip showing aria-selected. */} {SURFACE_PILLS.filter( (p) => p.value === surfaceFilter || p.value === 'all' || surfaceCounts[p.value] > 0, ).map((p) => ( ))}
{filtered.length === 0 ? (
{t('ds.emptyNoMatch')}
) : (
{filtered.map((s) => ( loadThumb(s.id)} onSelect={() => { trackDesignSystemsTemplateCardClick(analytics.track, { page_name: 'design_systems', area: 'templates_card', element: 'templates_card', templates_id: s.id, templates_type: s.source ?? 'library', }); onSelect(s.id); }} onPreview={() => { trackDesignSystemsTemplateCardClick(analytics.track, { page_name: 'design_systems', area: 'templates_card', element: 'templates_card', templates_id: s.id, templates_type: s.source ?? 'library', }); onPreview(s.id); }} /> ))}
)}
) : null} {primaryCollection === 'design-system' && designSystemCollection === 'enterprise' ? ( ) : null} {primaryCollection === 'template' && templateCollection === 'mine' ? (
Templates

Your templates

{templates.length === 0 ? (
No templates yet. Create one from any generated project via Share once template publishing is enabled.
) : (
{templates.map((template) => (
{template.name} {template.description?.trim() || 'Created from a project'}
{formatShortDate(template.createdAt)}
))}
)}
) : null} {primaryCollection === 'template' && templateCollection === 'enterprise' ? ( ) : null}
); } function ComingSoonPanel({ eyebrow, title, body, }: { eyebrow: string; title: string; body: string; }) { return (
{eyebrow}

{title}

Coming soon
{body}
); } interface CardProps { system: DesignSystemSummary; active: boolean; thumbHtml: string | null | undefined; onIntersect: () => void; onSelect: () => void; onPreview: () => void; } function DesignSystemCard({ system, active, thumbHtml, onIntersect, onSelect, onPreview, }: CardProps) { const { locale, t } = useI18n(); const ref = useRef(null); // Lazy-load the showcase iframe only when the card scrolls into the // viewport. With ~120 design systems we can't afford to mount every // iframe up front — even with `loading="lazy"`, srcDoc iframes ignore // the native lazy hint, so we gate via IntersectionObserver. useEffect(() => { if (thumbHtml !== undefined) return; const node = ref.current; if (!node || typeof IntersectionObserver === 'undefined') { onIntersect(); return; } const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { onIntersect(); observer.disconnect(); break; } } }, { rootMargin: '200px' }, ); observer.observe(node); return () => observer.disconnect(); }, [thumbHtml, onIntersect]); const localizedSummary = localizeDesignSystemSummary(locale, system); const categoryLabel = localizeDesignSystemCategory( locale, system.category || 'Uncategorized', ); return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(); } }} >
{ e.stopPropagation(); onPreview(); }} title={t('ds.previewTitle')} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); onPreview(); } }} > {thumbHtml ? (