Skip to content

Commit 73a6b78

Browse files
committed
feat(ui): surface deprecated product status
1 parent 3011e92 commit 73a6b78

9 files changed

Lines changed: 80 additions & 15 deletions

File tree

src/app/[locale]/clis/[slug]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export default async function CLIPage({
115115
category="CLI"
116116
categoryLabel={tShared('categories.singular.cli')}
117117
verified={cli.verified ?? false}
118+
deprecated={cli.deprecated ?? false}
118119
latestVersion={cli.latestVersion}
119120
license={cli.license}
120121
githubStars={getGithubStars('clis', cli.id)}

src/app/[locale]/clis/page.client.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useTranslations } from 'next-intl'
44
import { useMemo, useState } from 'react'
5+
import { DeprecatedBadge } from '@/components/controls/DeprecatedBadge'
56
import FilterSortBar from '@/components/controls/FilterSortBar'
67
import { VerifiedBadge } from '@/components/controls/VerifiedBadge'
78
import Footer from '@/components/Footer'
@@ -142,6 +143,7 @@ export default function CLIsPageClient({ locale }: Props) {
142143
<div className="flex items-center gap-[var(--spacing-xs)]">
143144
<h3 className="text-lg font-semibold tracking-tight">{cli.name}</h3>
144145
{cli.verified && <VerifiedBadge size="sm" />}
146+
{cli.deprecated && <DeprecatedBadge />}
145147
</div>
146148
<span className="text-lg text-[var(--color-text-muted)] group-hover:text-[var(--color-text)] group-hover:translate-x-1 transition-all">
147149

src/app/[locale]/open-source-rank/page.client.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useTranslations } from 'next-intl'
44
import { useMemo, useState } from 'react'
5+
import { DeprecatedBadge } from '@/components/controls/DeprecatedBadge'
56
import { VerifiedBadge } from '@/components/controls/VerifiedBadge'
67
import { Link } from '@/i18n/navigation'
78
import { clisData } from '@/lib/generated/clis'
@@ -21,6 +22,7 @@ type OpenSourceProject = {
2122
githubUrl: string | null
2223
websiteUrl: string | null
2324
verified?: boolean
25+
deprecated?: boolean
2426
}
2527

2628
function getLicenseDisplayName(license: string): string {
@@ -74,6 +76,7 @@ export function OpenSourceRankPage() {
7476
githubUrl: ide.githubUrl || null,
7577
websiteUrl: ide.websiteUrl || null,
7678
verified: ide.verified ?? false,
79+
deprecated: ide.deprecated ?? false,
7780
}
7881

7982
if (isProprietary && hasStars) {
@@ -104,6 +107,7 @@ export function OpenSourceRankPage() {
104107
githubUrl: cli.githubUrl || null,
105108
websiteUrl: cli.websiteUrl || null,
106109
verified: cli.verified ?? false,
110+
deprecated: cli.deprecated ?? false,
107111
}
108112

109113
if (isProprietary && hasStars) {
@@ -128,6 +132,7 @@ export function OpenSourceRankPage() {
128132
githubUrl: desktop.githubUrl || null,
129133
websiteUrl: desktop.websiteUrl || null,
130134
verified: desktop.verified ?? false,
135+
deprecated: desktop.deprecated ?? false,
131136
}
132137

133138
if (isProprietary && hasStars) proprietary.push(project)
@@ -155,6 +160,7 @@ export function OpenSourceRankPage() {
155160
githubUrl: ext.githubUrl || null,
156161
websiteUrl: ext.websiteUrl || null,
157162
verified: ext.verified ?? false,
163+
deprecated: ext.deprecated ?? false,
158164
}
159165

160166
if (isProprietary && hasStars) {
@@ -360,6 +366,7 @@ export function OpenSourceRankPage() {
360366
{project.name}
361367
</Link>
362368
{project.verified && <VerifiedBadge size="sm" />}
369+
{project.deprecated && <DeprecatedBadge />}
363370
</div>
364371
</td>
365372
<td className="px-[var(--spacing-sm)] py-[var(--spacing-sm)] text-sm">
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useTranslations } from 'next-intl'
2+
3+
export interface DeprecatedBadgeProps {
4+
size?: 'sm' | 'md' | 'lg'
5+
className?: string
6+
}
7+
8+
/** Displays a compact, localized lifecycle badge for deprecated entities. */
9+
export function DeprecatedBadge({ size = 'sm', className = '' }: DeprecatedBadgeProps) {
10+
const tShared = useTranslations('shared')
11+
const label = tShared('lifecycle.deprecated')
12+
const sizeClasses = {
13+
sm: 'h-4 px-1.5 text-[9px] tracking-[0.06em]',
14+
md: 'h-[18px] px-1.5 text-[10px] tracking-[0.06em]',
15+
lg: 'h-6 px-2 text-[11px] tracking-[0.08em]',
16+
}
17+
18+
return (
19+
<span
20+
className={`inline-flex box-border shrink-0 items-center whitespace-nowrap border border-current font-medium uppercase leading-none text-[#16a34a] ${sizeClasses[size]} ${className}`}
21+
title={label}
22+
>
23+
{label}
24+
</span>
25+
)
26+
}

src/components/controls/VerifiedBadge.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { BadgeCheck } from 'lucide-react'
33
export interface VerifiedBadgeProps {
44
/**
55
* Size variant of the badge
6-
* - sm: Small size for inline text (14px icon)
7-
* - md: Medium size for list items (16px icon)
8-
* - lg: Large size for hero sections (20px icon)
6+
* - sm: Small size for inline text (16px icon)
7+
* - md: Medium size for list items (18px icon)
8+
* - lg: Large size for hero sections (24px icon)
99
*/
1010
size?: 'sm' | 'md' | 'lg'
1111
/**
@@ -15,19 +15,19 @@ export interface VerifiedBadgeProps {
1515
}
1616

1717
/**
18-
* VerifiedBadge component displays a badge-check icon with low-saturation green color
18+
* VerifiedBadge component displays a badge-check icon with a restrained blue color
1919
* to indicate verified products, models, providers, or vendors.
2020
*/
2121
export function VerifiedBadge({ size = 'md', className = '' }: VerifiedBadgeProps) {
2222
const sizeClasses = {
23-
sm: 'w-3.5 h-3.5', // 14px
24-
md: 'w-4 h-4', // 16px
25-
lg: 'w-5 h-5', // 20px
23+
sm: 'size-4', // 16px
24+
md: 'size-[18px]', // 18px
25+
lg: 'size-6', // 24px
2626
}
2727

2828
return (
2929
<BadgeCheck
30-
className={`${sizeClasses[size]} text-[#16a34a] flex-shrink-0 ${className}`}
30+
className={`${sizeClasses[size]} text-[#2563eb] flex-shrink-0 ${className}`}
3131
aria-label="Verified"
3232
strokeWidth={2}
3333
/>

src/components/product/ProductHero.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useTranslations } from 'next-intl'
22
import React from 'react'
3+
import { DeprecatedBadge } from '@/components/controls/DeprecatedBadge'
34
import { VerifiedBadge } from '@/components/controls/VerifiedBadge'
45
import { renderLicense } from '@/lib/license'
56

@@ -11,6 +12,7 @@ export interface ProductHeroProps {
1112
category: 'CLI' | 'IDE' | 'DESKTOP' | 'EXTENSION' | 'PROVIDER' | 'MODEL' | 'VENDOR'
1213
categoryLabel?: string // Optional custom label for the badge
1314
verified?: boolean // Whether the product is verified
15+
deprecated?: boolean // Whether the entity has been superseded or is no longer recommended
1416

1517
// Metadata
1618
latestVersion?: string
@@ -60,6 +62,7 @@ export function ProductHero({
6062
category,
6163
categoryLabel,
6264
verified = false,
65+
deprecated = false,
6366
latestVersion,
6467
license,
6568
githubStars,
@@ -96,6 +99,7 @@ export function ProductHero({
9699
<div className="flex items-center justify-center gap-[var(--spacing-xs)]">
97100
<h1 className="text-5xl font-semibold tracking-[-0.04em] detail-page-h1">{name}</h1>
98101
{verified && <VerifiedBadge size="lg" />}
102+
{deprecated && <DeprecatedBadge size="lg" />}
99103
</div>
100104
<div className="absolute bottom-0 right-0 translate-x-[calc(100%+1rem)]">
101105
<div className="px-[var(--spacing-xs)] py-[2px] text-xs text-[var(--color-text-muted)] border-[1.5px] border-double border-[var(--color-border-strong)] whitespace-nowrap">

src/components/product/VendorMatrix.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useTranslations } from 'next-intl'
44
import { useMemo, useState } from 'react'
5+
import { DeprecatedBadge } from '@/components/controls/DeprecatedBadge'
56
import { Link } from '@/i18n/navigation'
67
import {
78
compareVendorMatrixRowsByProducts,
@@ -80,9 +81,12 @@ function MatrixCell({ products, category }: MatrixCellProps) {
8081
>
8182
<div className="flex flex-col justify-between">
8283
<div>
83-
<h4 className="font-medium text-sm tracking-tight mb-1 group-hover:text-[var(--color-text)] transition-colors line-clamp-2">
84-
{product.name}
85-
</h4>
84+
<div className="flex flex-wrap items-center gap-1 mb-1">
85+
<h4 className="font-medium text-sm tracking-tight group-hover:text-[var(--color-text)] transition-colors line-clamp-2">
86+
{product.name}
87+
</h4>
88+
{product.deprecated && <DeprecatedBadge />}
89+
</div>
8690
</div>
8791
</div>
8892
</Link>
@@ -99,9 +103,12 @@ function MatrixCell({ products, category }: MatrixCellProps) {
99103
href={product.path}
100104
className="flex-1 border border-[var(--color-border)] hover:border-[var(--color-border-strong)] transition-all p-[var(--spacing-sm)] bg-[var(--color-bg-subtle)] hover:bg-[var(--color-hover)] group"
101105
>
102-
<h4 className="font-medium text-sm tracking-tight group-hover:text-[var(--color-text)] transition-colors line-clamp-2">
103-
{product.name}
104-
</h4>
106+
<div className="flex flex-wrap items-center gap-1">
107+
<h4 className="font-medium text-sm tracking-tight group-hover:text-[var(--color-text)] transition-colors line-clamp-2">
108+
{product.name}
109+
</h4>
110+
{product.deprecated && <DeprecatedBadge />}
111+
</div>
105112
</Link>
106113
))}
107114
</div>
@@ -146,7 +153,10 @@ function MatrixCell({ products, category }: MatrixCellProps) {
146153
>
147154
<div className="flex items-center justify-between">
148155
<div className="flex-1 min-w-0">
149-
<h5 className="font-medium text-sm tracking-tight truncate">{product.name}</h5>
156+
<div className="flex items-center gap-1">
157+
<h5 className="font-medium text-sm tracking-tight truncate">{product.name}</h5>
158+
{product.deprecated && <DeprecatedBadge />}
159+
</div>
150160
</div>
151161
</div>
152162
</Link>

src/lib/landscape-data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface LandscapeProduct {
5050
vendor: string
5151
category: ProductCategory
5252
description: string
53+
deprecated?: boolean
5354
websiteUrl?: string
5455
docsUrl?: string
5556
githubUrl?: string | null
@@ -102,6 +103,7 @@ function ideToProduct(ide: ManifestIDE): LandscapeProduct {
102103
vendor: ide.vendor,
103104
category: 'ide',
104105
description: ide.description,
106+
deprecated: ide.deprecated,
105107
websiteUrl: ide.websiteUrl,
106108
docsUrl: ide.docsUrl || undefined,
107109
githubUrl: ide.githubUrl,
@@ -119,6 +121,7 @@ function cliToProduct(cli: ManifestCLI): LandscapeProduct {
119121
vendor: cli.vendor,
120122
category: 'cli',
121123
description: cli.description,
124+
deprecated: cli.deprecated,
122125
websiteUrl: cli.websiteUrl,
123126
docsUrl: cli.docsUrl || undefined,
124127
githubUrl: cli.githubUrl,
@@ -136,6 +139,7 @@ function desktopToProduct(desktop: ManifestDesktop): LandscapeProduct {
136139
vendor: desktop.vendor,
137140
category: 'desktop',
138141
description: desktop.description,
142+
deprecated: desktop.deprecated,
139143
websiteUrl: desktop.websiteUrl,
140144
docsUrl: desktop.docsUrl || undefined,
141145
githubUrl: desktop.githubUrl,
@@ -153,6 +157,7 @@ function extensionToProduct(ext: ManifestExtension): LandscapeProduct {
153157
vendor: ext.vendor,
154158
category: 'extension',
155159
description: ext.description,
160+
deprecated: ext.deprecated,
156161
websiteUrl: ext.websiteUrl,
157162
docsUrl: ext.docsUrl || undefined,
158163
githubUrl: ext.githubUrl,
@@ -170,6 +175,7 @@ function modelToProduct(model: ManifestModel): LandscapeProduct {
170175
vendor: model.vendor,
171176
category: 'model',
172177
description: model.description,
178+
deprecated: model.deprecated,
173179
websiteUrl: model.websiteUrl || undefined,
174180
docsUrl: model.docsUrl || undefined,
175181
path: buildManifestPath('models', model.id),
@@ -183,6 +189,7 @@ function providerToProduct(provider: ManifestProvider): LandscapeProduct {
183189
vendor: provider.vendor,
184190
category: 'provider',
185191
description: provider.description,
192+
deprecated: provider.deprecated,
186193
websiteUrl: provider.websiteUrl,
187194
docsUrl: provider.docsUrl || undefined,
188195
githubUrl: null, // Providers don't have githubUrl in schema

tests/landscape-data.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22
import {
3+
buildVendorMatrix,
34
compareVendorMatrixRowsByProducts,
45
LANDSCAPE_PRODUCT_CATEGORIES,
56
type LandscapeProduct,
@@ -104,4 +105,11 @@ describe('landscape matrix ordering', () => {
104105

105106
expect([zulu, alpha].sort(compareVendorMatrixRowsByProducts)).toEqual([alpha, zulu])
106107
})
108+
109+
it('propagates entity deprecation status to landscape products', () => {
110+
const google = buildVendorMatrix().find(row => row.vendorId === 'google')
111+
const geminiCli = google?.cells.cli.find(product => product.id === 'gemini-cli')
112+
113+
expect(geminiCli?.deprecated).toBe(true)
114+
})
107115
})

0 commit comments

Comments
 (0)