Skip to content

Commit c09544c

Browse files
Implement badge theming support across the application (#28)
* feat: Implement badge theming support across the application - 🎨 Added BadgeThemeContext for managing badge themes globally. - 🖌️ Introduced customizable badge styles and color schemes based on shield.io standards. - 🔧 Updated MCP, Packages, and Settings components to utilize the new badge theming. - 📦 Enhanced badge generation functions to accept theme parameters for consistent styling. - 🖼️ Improved badge previews in the Settings page to reflect selected themes. * feat: Enhance badge theme settings and styling - 🎨 Updated badge theme settings to use dropdowns for style and color scheme selection - 🖌️ Improved layout with grid adjustments for better responsiveness - 🔍 Added live preview for badge customization options * feat: Add badge theming support across profile and repository components - 🎨 Integrate `useBadgeTheme` in Profile and Repository components - 🛠️ Update badge generation functions to accept theme parameters - 📄 Enhance badge styles for various platforms based on theme settings * feat: Update badge styles for consistency - 🎨 Change badge URLs to use 'flat-square' style for better aesthetics in extension badges - 🔧 Modify profile badge test to use 'social' style for YouTube subscribers badge - ✅ Ensure repository badge test includes 'flat-square' style for stars badge
1 parent 00a4f97 commit c09544c

19 files changed

Lines changed: 876 additions & 128 deletions

src/BadgeThemeContext.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { createContext, useContext, useState, useEffect } from 'react'
2+
import type { ReactNode } from 'react'
3+
import type { BadgeTheme } from './types/badgeTheme'
4+
import { DEFAULT_BADGE_THEMES } from './types/badgeTheme'
5+
6+
interface BadgeThemeContextType {
7+
badgeTheme: BadgeTheme
8+
setBadgeTheme: (theme: BadgeTheme) => void
9+
updateBadgeTheme: (updates: Partial<BadgeTheme>) => void
10+
}
11+
12+
const BadgeThemeContext = createContext<BadgeThemeContextType | undefined>(undefined)
13+
14+
// eslint-disable-next-line react-refresh/only-export-components
15+
export function useBadgeTheme() {
16+
const context = useContext(BadgeThemeContext)
17+
if (context === undefined) {
18+
throw new Error('useBadgeTheme must be used within a BadgeThemeProvider')
19+
}
20+
return context
21+
}
22+
23+
interface BadgeThemeProviderProps {
24+
children: ReactNode
25+
}
26+
27+
export function BadgeThemeProvider({ children }: BadgeThemeProviderProps) {
28+
const [badgeTheme, setBadgeTheme] = useState<BadgeTheme>(() => {
29+
const saved = localStorage.getItem('badge-theme-settings')
30+
return saved ? JSON.parse(saved) : {
31+
style: 'flat-square',
32+
colorScheme: 'default',
33+
customColors: DEFAULT_BADGE_THEMES.default.customColors,
34+
logoColor: 'white',
35+
showLogo: true
36+
}
37+
})
38+
39+
useEffect(() => {
40+
localStorage.setItem('badge-theme-settings', JSON.stringify(badgeTheme))
41+
}, [badgeTheme])
42+
43+
const updateBadgeTheme = (updates: Partial<BadgeTheme>) => {
44+
setBadgeTheme(prev => ({ ...prev, ...updates }))
45+
}
46+
47+
return (
48+
<BadgeThemeContext.Provider value={{ badgeTheme, setBadgeTheme, updateBadgeTheme }}>
49+
{children}
50+
</BadgeThemeContext.Provider>
51+
)
52+
}

src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
22
import { createRoot } from 'react-dom/client'
33
import './index.css'
44
import App from './App.tsx'
5+
import { BadgeThemeProvider } from './BadgeThemeContext'
56

67
createRoot(document.getElementById('root')!).render(
78
<StrictMode>
8-
<App />
9+
<BadgeThemeProvider>
10+
<App />
11+
</BadgeThemeProvider>
912
</StrictMode>,
1013
)

src/pages/Extensions.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import { generateExtensionBadges, parseExtensionInput } from '../utils/extension
55
import SearchDropdown from '../components/SearchDropdown'
66
import type { SortBy } from '../utils/marketplaceApi'
77
import RequestBadge from '../components/RequestBadge'
8+
import { useBadgeTheme } from '../BadgeThemeContext'
89

910
type BadgeVariant = 'stable' | 'insiders' | 'install' | 'rating' | 'installs' | 'downloads' | 'version' | 'lastUpdated' | 'releaseDate' | 'about' | 'all'
1011
type InputMode = 'manual' | 'search'
1112

1213
function Extensions() {
14+
const { badgeTheme } = useBadgeTheme()
15+
const [badgeText] = useState(() => localStorage.getItem('readme-default-badge-text') || 'Install in')
1316
const [inputValue, setInputValue] = useState('')
1417
const [inputMode, setInputMode] = useState<InputMode>('search')
1518
const [searchQuery, setSearchQuery] = useState('')
@@ -53,7 +56,7 @@ function Extensions() {
5356
const parsed = parseExtensionInput(extensionId)
5457
if (parsed.extensionId) {
5558
setError(null)
56-
setBadgeData(generateExtensionBadges(parsed.extensionId))
59+
setBadgeData(generateExtensionBadges(parsed.extensionId, badgeTheme, badgeText))
5760
}
5861
}
5962

@@ -82,7 +85,7 @@ function Extensions() {
8285

8386
setError(null)
8487
setInfo(parsed.message ?? null)
85-
setBadgeData(generateExtensionBadges(parsed.extensionId))
88+
setBadgeData(generateExtensionBadges(parsed.extensionId, badgeTheme, badgeText))
8689
}
8790

8891
const handleCopy = async (variant: BadgeVariant) => {

src/pages/MCP.tsx

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import MCPSearchDropdown from '../components/MCPSearchDropdown'
44
import { parseRuntimeConfig } from '../utils/mcpRegistryApi'
55
import type { MCPSearchResult } from '../utils/mcpRegistryApi'
66
import RequestBadge from '../components/RequestBadge'
7+
import { useBadgeTheme } from '../BadgeThemeContext'
8+
import {
9+
generateVSCodeBadge,
10+
generateVSCodeInsidersBadge,
11+
generateVisualStudioBadge,
12+
generateCursorBadge,
13+
type ConfigWithInputs
14+
} from '../utils/mcpBadgeGenerator'
715

816
type ConfigType = 'http' | 'docker' | 'local' | 'npx' | 'uvx' | 'dnx';
917

@@ -52,6 +60,7 @@ interface StandaloneInput {
5260
}
5361

5462
function MCP() {
63+
const { badgeTheme } = useBadgeTheme()
5564
const [serverName, setServerName] = useState('')
5665
const [configType, setConfigType] = useState<ConfigType>('http')
5766
const [serverUrl, setServerUrl] = useState('')
@@ -897,62 +906,32 @@ function MCP() {
897906
const generateMarkdown = (): string => {
898907
if (!serverName) return '';
899908

900-
const configForBadge = getConfigForBadge();
901-
const encodedConfig = encodeConfig(configForBadge);
902-
const inputs = getInputsForBadge();
903-
const encodedInputs = inputs.length > 0 ? encodeURIComponent(JSON.stringify(inputs)) : '';
909+
const fullConfig = generateFullConfig();
910+
const configForBadge: ConfigWithInputs = {
911+
...generateConfig(),
912+
...(fullConfig.inputs && fullConfig.inputs.length > 0 ? { inputs: fullConfig.inputs } : {})
913+
};
904914
const badges: string[] = [];
905915

906-
const customBadgeText = badgeText.replace(/\s/g, '_');
907-
908916
if (includeVSCode) {
909-
let vscodeUrl = `https://vscode.dev/redirect/mcp/install?name=${encodeURIComponent(serverName)}`;
910-
if (encodedInputs) {
911-
vscodeUrl += `&inputs=${encodedInputs}`;
912-
}
913-
vscodeUrl += `&config=${encodedConfig}`;
914-
vscodeUrl = escapeUrlForMarkdown(vscodeUrl);
915-
badges.push(`[![Install in VS Code](https://img.shields.io/badge/${customBadgeText}-VS_Code-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](${vscodeUrl})`);
917+
badges.push(generateVSCodeBadge(serverName, configForBadge, badgeText, badgeTheme));
916918
}
917919

918920
if (includeVSCodeInsiders) {
919-
let vscodeInsidersUrl = `https://insiders.vscode.dev/redirect/mcp/install?name=${encodeURIComponent(serverName)}`;
920-
if (encodedInputs) {
921-
vscodeInsidersUrl += `&inputs=${encodedInputs}`;
922-
}
923-
vscodeInsidersUrl += `&config=${encodedConfig}&quality=insiders`;
924-
vscodeInsidersUrl = escapeUrlForMarkdown(vscodeInsidersUrl);
925-
badges.push(`[![Install in VS Code Insiders](https://img.shields.io/badge/${customBadgeText}-VS_Code_Insiders-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](${vscodeInsidersUrl})`);
921+
badges.push(generateVSCodeInsidersBadge(serverName, configForBadge, badgeText, badgeTheme));
926922
}
927923

928924
if (includeVisualStudio) {
929-
let vsUrl = `https://vs-open.link/mcp-install?${encodedConfig}`;
930-
vsUrl = escapeUrlForMarkdown(vsUrl);
931-
badges.push(`[![Install in Visual Studio](https://img.shields.io/badge/${customBadgeText}-Visual_Studio-C16FDE?style=flat-square&logo=visualstudio&logoColor=white)](${vsUrl})`);
925+
badges.push(generateVisualStudioBadge(serverName, configForBadge, badgeText, badgeTheme));
932926
}
933927

934928
if (includeCursor) {
935-
// Use Cursor badge with same style as VS Code badges but black background
936-
const fullConfig = generateFullConfig();
937-
const configWithName = {
938-
name: serverName,
939-
...generateConfig(),
940-
...(fullConfig.inputs && fullConfig.inputs.length > 0 ? { inputs: fullConfig.inputs } : {})
941-
};
942-
const base64Config = btoa(JSON.stringify(configWithName));
943-
let cursorUrl = `https://cursor.com/en/install-mcp?name=${encodeURIComponent(serverName)}&config=${base64Config}`;
944-
cursorUrl = escapeUrlForMarkdown(cursorUrl);
945-
badges.push(`[![Install in Cursor](https://img.shields.io/badge/${customBadgeText}-Cursor-000000?style=flat-square&logoColor=white)](${cursorUrl})`);
929+
badges.push(generateCursorBadge(serverName, configForBadge, badgeText, badgeTheme));
946930
}
947931

948932
if (includeGoose) {
949933
// Use Goose's official badge format from Playwright repository
950-
const fullConfig = generateFullConfig();
951-
const configWithName = {
952-
name: serverName,
953-
...generateConfig(),
954-
...(fullConfig.inputs && fullConfig.inputs.length > 0 ? { inputs: fullConfig.inputs } : {})
955-
};
934+
const configWithName = configForBadge;
956935
const args = configWithName.args ? configWithName.args.join('%20') : '';
957936
const cmd = configWithName.command || '';
958937
let gooseUrl = `https://block.github.io/goose/extension?cmd=${encodeURIComponent(cmd)}&arg=${encodeURIComponent(args)}&id=${encodeURIComponent(serverName)}&name=${encodeURIComponent(serverName)}&description=MCP%20Server%20for%20${encodeURIComponent(serverName)}`;
@@ -962,12 +941,7 @@ function MCP() {
962941

963942
if (includeLMStudio) {
964943
// Use LM Studio's official badge format from Playwright repository
965-
const fullConfig = generateFullConfig();
966-
const configWithName = {
967-
name: serverName,
968-
...generateConfig(),
969-
...(fullConfig.inputs && fullConfig.inputs.length > 0 ? { inputs: fullConfig.inputs } : {})
970-
};
944+
const configWithName = configForBadge;
971945
const base64Config = btoa(JSON.stringify(configWithName));
972946
let lmstudioUrl = `https://lmstudio.ai/install-mcp?name=${encodeURIComponent(serverName)}&config=${base64Config}`;
973947
lmstudioUrl = escapeUrlForMarkdown(lmstudioUrl);

src/pages/Packages.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import styles from './Packages.module.css'
44
import { parsePackageInput, generatePackageBadges, getInstallCommands, type PackageManager } from '../utils/packageBadge'
55
import PackageSearchDropdown from '../components/PackageSearchDropdown'
66
import RequestBadge from '../components/RequestBadge'
7+
import { useBadgeTheme } from '../BadgeThemeContext'
78

89
type CopyTarget = 'version' | 'versionPrerelease' | 'downloads' | 'downloadsMonthly' | 'downloadsRecent' | 'combined' | 'commands' | `command-${number}`
910
type InputMode = 'manual' | 'search'
1011
type Registry = 'npm' | 'pypi' | 'nuget' | 'rubygems' | 'crates' | 'maven'
1112

1213
function Packages() {
14+
const { badgeTheme } = useBadgeTheme()
1315
const [inputValue, setInputValue] = useState('')
1416
const [inputMode, setInputMode] = useState<InputMode>('search')
1517
const [searchQuery, setSearchQuery] = useState('')
@@ -87,7 +89,7 @@ function Packages() {
8789
setGroupId(parts[0])
8890
setArtifactId(parts[1])
8991
setManualManager('maven')
90-
const badges = generatePackageBadges('maven', null, parts[0], parts[1])
92+
const badges = generatePackageBadges('maven', null, parts[0], parts[1], badgeTheme)
9193
const cmds = getInstallCommands('maven', null, parts[0], parts[1])
9294
if (badges) {
9395
setError(null)
@@ -99,7 +101,7 @@ function Packages() {
99101
} else {
100102
// For other package managers, generate badges automatically
101103
setManualManager(manager)
102-
const badges = generatePackageBadges(manager, packageName)
104+
const badges = generatePackageBadges(manager, packageName, undefined, undefined, badgeTheme)
103105
const cmds = getInstallCommands(manager, packageName)
104106
if (badges) {
105107
setError(null)
@@ -144,7 +146,7 @@ function Packages() {
144146

145147
// For Maven with manual input
146148
if (manualManager === 'maven' && groupId && artifactId) {
147-
const badges = generatePackageBadges('maven', null, groupId, artifactId)
149+
const badges = generatePackageBadges('maven', null, groupId, artifactId, badgeTheme)
148150
const cmds = getInstallCommands('maven', null, groupId, artifactId)
149151

150152
if (badges) {
@@ -191,7 +193,8 @@ function Packages() {
191193
effectiveManager,
192194
parsed.packageId,
193195
parsed.groupId,
194-
parsed.artifactId
196+
parsed.artifactId,
197+
badgeTheme
195198
)
196199
const cmds = getInstallCommands(
197200
effectiveManager,

src/pages/Profile.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
type BadgeConfig,
88
type BadgeType,
99
} from '../utils/profileBadge'
10+
import { useBadgeTheme } from '../BadgeThemeContext'
1011

1112
function Profile() {
13+
const { badgeTheme } = useBadgeTheme()
1214
const [defaultUsername, setDefaultUsername] = useState('')
1315
const [error, setError] = useState<string | null>(null)
1416
const [copiedBadge, setCopiedBadge] = useState<string | null>(null)
@@ -190,7 +192,7 @@ function Profile() {
190192
setError(null)
191193
}
192194

193-
const allBadges = generateProfileBadges(badgeConfigs)
195+
const allBadges = generateProfileBadges(badgeConfigs, badgeTheme)
194196

195197
// Reordering: maintain order of enabled badges
196198
const [badgeOrder, setBadgeOrder] = useState<BadgeType[]>([])

src/pages/Repository.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import {
99
type BadgeConfig,
1010
} from '../utils/repositoryBadge'
1111
import RequestBadge from '../components/RequestBadge'
12+
import { useBadgeTheme } from '../BadgeThemeContext'
1213

1314
type InputMode = 'manual' | 'search'
1415
type PreviewMode = 'grouped' | 'flat'
1516

1617
const WORKFLOW_PRESETS = ['ci.yml', 'test.yml', 'build.yml', 'deploy.yml', 'release.yml', 'lint.yml']
1718

1819
function Repository() {
20+
const { badgeTheme } = useBadgeTheme()
1921
const [inputValue, setInputValue] = useState('')
2022
const [inputMode] = useState<InputMode>('manual') // Search not implemented yet
2123
const [error, setError] = useState<string | null>(null)
@@ -298,7 +300,7 @@ function Repository() {
298300

299301
const repoInfo = parseRepositoryInput(inputValue)
300302
const allConfigs = [...badgeConfigs, ...workflowConfigs]
301-
const badges = repoInfo ? generateRepositoryBadges(repoInfo, allConfigs, defaultBranch || 'main') : []
303+
const badges = repoInfo ? generateRepositoryBadges(repoInfo, allConfigs, defaultBranch || 'main', badgeTheme) : []
302304

303305
const essentialBadges = badges.filter(b => ['stars', 'license', 'contributors', 'release','coverage','openssf'].includes(b.type))
304306
const workflowBadges = badges.filter(b => b.type === 'workflow')

0 commit comments

Comments
 (0)