diff --git a/components/contributors/BadgeDisplay.css b/components/contributors/BadgeDisplay.css index b2abcfb4..8e34c75f 100644 --- a/components/contributors/BadgeDisplay.css +++ b/components/contributors/BadgeDisplay.css @@ -60,6 +60,101 @@ align-items: stretch; } +/* Overflow chip */ +.badge-overflow-chip { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 12px; + border: 2px dashed var(--theme-popup-border); + background: var(--quote-bg, rgba(0, 0, 0, 0.05)); + font-size: 0.6875rem; + font-weight: 700; + color: var(--fg); + opacity: 0.7; + cursor: default; + transition: opacity 0.2s ease, border-color 0.2s ease; + z-index: 1; +} + +.badge-overflow-chip:hover { + opacity: 1; + border-color: var(--vocs-color_codeInlineBg); + z-index: 99999; +} + +.badge-overflow-chip::before { + content: ''; + position: absolute; + bottom: 100%; + left: -8px; + right: -8px; + height: 14px; +} + +.badge-overflow-tooltip { + position: absolute; + bottom: calc(100% + 14px); + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, #1f2937 0%, #111827 100%); + padding: 0.625rem; + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.1); + z-index: 999999; + opacity: 0; + visibility: hidden; +} + +.badge-overflow-tooltip { width: max-content; } + +.badge-overflow-tooltip::before { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 7px solid transparent; + border-top-color: #111827; +} + +.badge-overflow-chip:hover .badge-overflow-tooltip, +.badge-overflow-tooltip:hover { + opacity: 1; + visibility: visible; +} + +.badge-overflow-tooltip-grid { + display: grid; + gap: 0.5rem; + justify-items: center; +} + +.badge-overflow-tooltip-grid[data-cols="1"] { grid-template-columns: repeat(1, 48px); } +.badge-overflow-tooltip-grid[data-cols="2"] { grid-template-columns: repeat(2, 48px); } +.badge-overflow-tooltip-grid[data-cols="3"] { grid-template-columns: repeat(3, 48px); } +.badge-overflow-tooltip-grid[data-cols="4"] { grid-template-columns: repeat(4, 48px); } + +.badge-overflow-tooltip .badge-wrapper { + z-index: 1; +} + +.badge-overflow-tooltip .badge-wrapper:hover { + z-index: 99999; +} + +:root:not(.dark) .badge-overflow-tooltip { + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12), 0 0 0 1px rgba(15, 23, 42, 0.08); +} + +:root:not(.dark) .badge-overflow-tooltip::before { + border-top-color: #ffffff; +} + /* Badge Wrapper */ .badge-wrapper { position: relative; diff --git a/components/contributors/BadgeDisplay.tsx b/components/contributors/BadgeDisplay.tsx index a85643b9..798e4939 100644 --- a/components/contributors/BadgeDisplay.tsx +++ b/components/contributors/BadgeDisplay.tsx @@ -71,6 +71,8 @@ function getBadgeDateLabel(badge: Badge): 'Earned' | 'Last active' { return badge.lastActive ? 'Last active' : 'Earned'; } +const COMPACT_MAX_VISIBLE = 5; + interface BadgeDisplayProps { contributorSlug?: string; badges?: Badge[]; @@ -79,6 +81,66 @@ interface BadgeDisplayProps { layout?: 'grid' | 'stack'; } +function BadgeCard({ badge, index, compact }: { badge: Badge; index: number; compact: boolean }) { + const config = getBadgeConfig(badge.name); + const effectiveDate = getBadgeDate(badge); + const dateLabel = getBadgeDateLabel(badge); + const isNew = isNewlyEarned(effectiveDate); + const badgeDate = formatDate(effectiveDate); + const badgeKey = `${badge.name}-${badge.framework || ''}-${index}`; + const badgeLabel = badge.name === 'Framework-Steward' ? 'Steward' : config.label; + const badgeDescription = badge.framework && badge.name === 'Framework-Steward' + ? `Steward of the ${badge.framework} framework` + : config.description; + + return ( +
+
+ + {isNew && ( +
+ +
+ )} + {!compact && ( +
+ {config.tier === 'legendary' && '👑'} + {config.tier === 'epic' && '💎'} + {config.tier === 'rare' && '⭐'} +
+ )} +
+ +
+
+ {badgeLabel} + + {config.tier?.toUpperCase()} + +
+

{badgeDescription}

+ {badgeDate && ( +
+ + {isNew && ✨ NEW} + {dateLabel} {badgeDate} + +
+ )} +
+
+ ); +} + export function BadgeDisplay({ contributorSlug, badges, @@ -93,7 +155,7 @@ export function BadgeDisplay({ if (badges) { displayBadges = badges.filter(b => b.name && b.name.trim() !== ''); } else if (contributorSlug) { - const contributors = contributorsData as Record; + const contributors = contributorsData as unknown as Record; const contributor = contributors[contributorSlug]; if (contributor?.badges) { displayBadges = contributor.badges.filter(b => b.name && b.name.trim() !== ''); @@ -102,12 +164,22 @@ export function BadgeDisplay({ if (displayBadges.length === 0) return null; - // Sort badges chronologically by date (newest first) - const sortedBadges = [...displayBadges].sort((a, b) => { - const dateA = new Date(getBadgeDate(a) || '1970-01-01').getTime(); - const dateB = new Date(getBadgeDate(b) || '1970-01-01').getTime(); - return dateB - dateA; - }); + const byDateDesc = (a: Badge, b: Badge) => + new Date(getBadgeDate(b) || '1970-01-01').getTime() - new Date(getBadgeDate(a) || '1970-01-01').getTime(); + + let visibleBadges: Badge[]; + let hiddenBadges: Badge[]; + + if (compact) { + const roleBadges = displayBadges.filter(b => getBadgeConfig(b.name).category === 'role').sort(byDateDesc); + const nonRoleBadges = displayBadges.filter(b => getBadgeConfig(b.name).category !== 'role').sort(byDateDesc); + const nonRoleSlots = Math.max(0, COMPACT_MAX_VISIBLE - roleBadges.length); + visibleBadges = [...nonRoleBadges.slice(0, nonRoleSlots), ...roleBadges]; + hiddenBadges = nonRoleBadges.slice(nonRoleSlots); + } else { + visibleBadges = [...displayBadges].sort(byDateDesc); + hiddenBadges = []; + } return (
- {sortedBadges.map((badge, index) => { - const config = getBadgeConfig(badge.name); - const effectiveDate = getBadgeDate(badge); - const dateLabel = getBadgeDateLabel(badge); - const isNew = isNewlyEarned(effectiveDate); - const badgeDate = formatDate(effectiveDate); - const badgeKey = `${badge.name}-${badge.framework || ''}-${index}`; - const badgeLabel = badge.name === 'Framework-Steward' - ? 'Steward' - : config.label; - const badgeDescription = badge.framework && badge.name === 'Framework-Steward' - ? `Steward of the ${badge.framework} framework` - : config.description; - + {visibleBadges.map((badge, index) => ( + + ))} + {hiddenBadges.length > 0 && (() => { + const count = hiddenBadges.length; + const cols = count <= 3 ? count : count === 4 ? 2 : 4; return ( -
-
- - {isNew && ( -
- -
- )} - {!compact && ( -
- {config.tier === 'legendary' && '👑'} - {config.tier === 'epic' && '💎'} - {config.tier === 'rare' && '⭐'} -
- )} -
- -
-
- {badgeLabel} - - {config.tier?.toUpperCase()} - +
+ +{count} +
+
+ {hiddenBadges.map((badge, index) => ( + + ))}
-

{badgeDescription}

- {badgeDate && ( -
- - {isNew && ✨ NEW} - {dateLabel} {badgeDate} - -
- )}
); - })} + })()}
); diff --git a/components/contributors/Contributors.css b/components/contributors/Contributors.css index 2e51111a..b3dc1e0f 100644 --- a/components/contributors/Contributors.css +++ b/components/contributors/Contributors.css @@ -122,8 +122,8 @@ /* Grid Layout */ .contributors-page-list { display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 1.5rem; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 1rem; padding: 0 0.5rem; max-width: 1400px; } @@ -133,7 +133,7 @@ background: var(--sidebar-bg); border: 1px solid var(--theme-popup-border); border-radius: 6px; - padding: 1rem; + padding: 0.75rem 0.625rem; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); transition: all 0.2s ease, z-index 0s 0.2s; display: flex; @@ -142,8 +142,8 @@ text-align: center; font-size: 0.875rem; position: relative; - min-height: 280px; - min-width: 250px; + min-height: 0; + min-width: 0; isolation: isolate; } @@ -170,11 +170,11 @@ } .contributors-page-avatar { - width: 80px; - height: 80px; + width: 60px; + height: 60px; border-radius: 50%; object-fit: cover; - border: 3px solid var(--theme-popup-border); + border: 2px solid var(--theme-popup-border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); background-color: var(--sidebar-bg); transition: all 0.3s ease; @@ -184,15 +184,15 @@ .avatar-badge { position: absolute; bottom: -4px; - right: calc(50% - 44px - 8px); - width: 32px; - height: 32px; + right: calc(50% - 34px - 6px); + width: 26px; + height: 26px; display: flex; align-items: center; justify-content: center; - font-size: 1.125rem; + font-size: 0.875rem; background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); - border: 3px solid var(--sidebar-bg); + border: 2px solid var(--sidebar-bg); border-radius: 50%; box-shadow: 0 4px 12px rgba(251, 191, 36, 0.4); z-index: 3; @@ -207,9 +207,9 @@ .role-badge-overlay { position: absolute; bottom: -4px; - right: calc(50% - 48px); - width: 32px; - height: 32px; + right: calc(50% - 38px); + width: 26px; + height: 26px; z-index: 3; cursor: pointer; } @@ -293,14 +293,14 @@ display: flex; flex-direction: column; align-items: center; - gap: 0.5rem; + gap: 0.25rem; width: 100%; - margin-top: 10px; + margin-top: 6px; } .contributors-page-name { font-weight: 600; - font-size: 1.1rem; + font-size: 0.875rem; word-break: break-word; color: var(--fg); line-height: 1.3; @@ -410,9 +410,9 @@ .contributors-page-social { display: flex; - gap: 0.6rem; + gap: 0.5rem; justify-content: center; - margin: 0.8rem 0 0.3rem; + margin: 0.5rem 0 0.2rem; } .contributor-link { @@ -472,10 +472,28 @@ justify-content: center; } -/* Elevate card when badge tooltip is shown */ -.contributors-page-card:has(.badge-wrapper:hover) { +/* Elevate card when any tooltip/popup is active */ +.contributors-page-card:has(.badge-wrapper:hover), +.contributors-page-card:has(.badge-overflow-chip:hover), +.contributors-page-card:has(.role-badge-overlay:hover), +.contributors-page-card:has(.avatar-wrapper:hover), +.contributors-page-card:has(.contributor-hover-card:hover) { z-index: 99999; transition-delay: 0s; + isolation: auto; +} + + +/* Push role overlay behind tooltips when badges or overflow chip are hovered */ +.contributors-page-card:has(.badge-wrapper:hover) .role-badge-overlay, +.contributors-page-card:has(.badge-overflow-chip:hover) .role-badge-overlay { + z-index: 0; +} + +/* Ensure badges section sits above the avatar section within the card */ +.contributors-page-badges { + position: relative; + z-index: 2; } @@ -521,6 +539,120 @@ border-bottom: 1px solid #ffffff17; } +/* Contributor hover card (shown when hovering avatar) */ +.contributor-hover-card { + position: absolute; + /* sit right next to the avatar: 50% of avatar-wrapper (card center) + half-avatar (30px) */ + left: calc(50% + 50px); + top: 50%; + transform: translateY(-50%); + width: 230px; + background: linear-gradient(135deg, #1f2937 0%, #111827 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + padding: 0.875rem; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.06); + z-index: 999999; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.18s ease, visibility 0.18s ease; +} + + +/* Show when avatar is hovered — but suppress if the role badge is specifically hovered */ +.avatar-wrapper:hover:not(:has(.role-badge-overlay:hover)) .contributor-hover-card, +.contributor-hover-card:hover { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +.hover-card-header { + display: flex; + align-items: center; + gap: 0.625rem; + margin-bottom: 0.625rem; +} + +.hover-card-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(255, 255, 255, 0.15); + flex-shrink: 0; +} + +.hover-card-identity { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 0; +} + +.hover-card-name { + font-weight: 700; + font-size: 0.875rem; + color: #f9fafb; + line-height: 1.2; +} + +.hover-card-job-title { + font-size: 0.7rem; + color: #9ca3af; + font-style: italic; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.hover-card-company { + display: inline-block; + font-size: 0.6875rem; + font-weight: 600; + color: #d1d5db; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 4px; + padding: 0.2rem 0.5rem; + margin-bottom: 0.5rem; +} + +.hover-card-steward { + font-size: 0.6875rem; + font-weight: 600; + color: #60a5fa; + margin-bottom: 0.5rem; +} + +.hover-card-description { + font-size: 0.75rem; + color: #9ca3af; + line-height: 1.5; + margin: 0; + border-top: 1px solid rgba(255, 255, 255, 0.07); + padding-top: 0.5rem; +} + +:root:not(.dark) .contributor-hover-card { + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + border-color: rgba(15, 23, 42, 0.08); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12), 0 0 0 1px rgba(15, 23, 42, 0.06); +} + +:root:not(.dark) .hover-card-name { color: #111827; } +:root:not(.dark) .hover-card-job-title { color: #6b7280; } +:root:not(.dark) .hover-card-company { + color: #374151; + background: rgba(15, 23, 42, 0.05); + border-color: rgba(15, 23, 42, 0.1); +} +:root:not(.dark) .hover-card-steward { color: #2563eb; } +:root:not(.dark) .hover-card-description { color: #4b5563; border-top-color: rgba(15, 23, 42, 0.08); } +:root:not(.dark) .hover-card-avatar { border-color: rgba(15, 23, 42, 0.15); } + /* Corner Accent for elite contributors */ .corner-accent { position: absolute; @@ -574,25 +706,25 @@ } .contributors-page-list { - grid-template-columns: 1fr; - gap: 1.5rem; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 0.75rem; padding: 0; } .contributors-page-card { - padding: 1.5rem; + padding: 0.625rem 0.5rem; } .contributors-page-avatar { - width: 72px; - height: 72px; + width: 52px; + height: 52px; } .avatar-badge { - right: calc(50% - 36px - 8px); - width: 28px; - height: 28px; - font-size: 1rem; + right: calc(50% - 30px - 6px); + width: 22px; + height: 22px; + font-size: 0.75rem; } .contributors-page-badges { @@ -603,8 +735,8 @@ @media (min-width: 769px) and (max-width: 1024px) { .contributors-page-list { - grid-template-columns: repeat(2, 1fr); - gap: 1.5rem; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.875rem; } .stat-number { diff --git a/components/contributors/Contributors.tsx b/components/contributors/Contributors.tsx index a4cdadc0..20fbf08b 100644 --- a/components/contributors/Contributors.tsx +++ b/components/contributors/Contributors.tsx @@ -82,6 +82,39 @@ function RoleBadgeOverlay({ role, badges }: { role: string; badges: Badge[] }) { ); } +function ContributorHoverCard({ contributor }: { contributor: Contributor }) { + const hasContent = contributor.company || contributor.job_title || contributor.description; + if (!hasContent) return null; + const frameworks = getStewardFrameworks(contributor.badges || []); + return ( +
+
+ {contributor.name} +
+
{contributor.name}
+ {contributor.job_title && ( +
{contributor.job_title}
+ )} +
+
+ {contributor.company && ( +
{contributor.company}
+ )} + {frameworks.length > 0 && ( +
Steward of {formatFrameworks(frameworks)}
+ )} + {contributor.description && ( +

{contributor.description}

+ )} +
+ ); +} + // Helper component for steward info function StewardInfo({ badges }: { badges: Badge[] }) { const frameworks = getStewardFrameworks(badges); @@ -160,7 +193,7 @@ function slugify(text: string): string { export function Contributors() { // Convert JSON object to array - const contributors = Object.values(contributorsData as Record); + const contributors = Object.values(contributorsData as unknown as Record); const groups: ContributorGroup[] = [ { @@ -215,7 +248,7 @@ export function Contributors() { id={contributor.name.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9_-]/g, '')} style={{ '--card-index': index } as React.CSSProperties} > - {/* Avatar with role badge */} + {/* Avatar with role badge and profile hover card */}
)} +
{/* Header with name */} @@ -235,28 +269,10 @@ export function Contributors() {
- {/* Content area - flexible section */} -
- {/* Company */} -
- {contributor.company || '\u00A0'} -
- - {/* Job Title */} -
- {contributor.job_title || '\u00A0'} -
- - {/* Steward info */} + {/* Steward info */} + {contributor.role === 'steward' && ( - - {/* Description - only show for non-stewards */} - {contributor.description && contributor.role !== "steward" && ( -
- {contributor.description} -
- )} -
+ )} {/* Badges display */} {contributor.badges && contributor.badges.length > 0 && ( diff --git a/docs/pages/contribute/spotlight-zone.mdx b/docs/pages/contribute/spotlight-zone.mdx index b7ce2368..66feb1f8 100644 --- a/docs/pages/contribute/spotlight-zone.mdx +++ b/docs/pages/contribute/spotlight-zone.mdx @@ -11,6 +11,8 @@ import { Contributors, ContributeFooter, TagFilter, TagProvider, BadgeLegend } f This is the current list of individuals who have made substantial contributions to the project and deserve recognition. +To understand what each badge represents, refer to the [Badge Legend](#badge-legend) at the bottom of this page. + diff --git a/utils/fetched-tags.json b/utils/fetched-tags.json index 94e7693b..a405a185 100644 --- a/utils/fetched-tags.json +++ b/utils/fetched-tags.json @@ -1269,45 +1269,45 @@ ] }, "sectionMappings": { - "Community Management": "community-management", + "AI Security": "ai-security", "Awareness": "awareness", - "Operational Security": "opsec", - "OpSec Core Concepts": "opsec", - "While Traveling": "opsec", - "Wallet Security": "wallet-security", - "Signing & Verification": "wallet-security", - "Multisig for Protocols": "multisig-for-protocols", - "Multisig Administration": "multisig-for-protocols", - "Operational Runbooks": "multisig-for-protocols", - "For Signers": "multisig-for-protocols", + "Community Management": "community-management", + "DevSecOps": "devsecops", + "Isolation & Sandboxing": "devsecops", + "DPRK IT Workers": "dprk-it-workers", + "Encryption": "encryption", + "ENS": "ens", "External Security Reviews": "external-security-reviews", "Smart Contract Audits": "external-security-reviews", - "Vulnerability Disclosure": "vulnerability-disclosure", - "Infrastructure": "infrastructure", - "Domain & DNS Security": "infrastructure", - "Monitoring": "monitoring", "Front-End/Web Application": "front-end-web-app", + "Governance": "governance", + "Identity and Access Management IAM": "iam", "Incident Management": "incident-management", "Playbooks": "incident-management", "Incident Response Template": "incident-management", "Templates": "incident-management", "Runbooks": "incident-management", - "Threat Modeling": "threat-modeling", - "DPRK IT Workers": "dprk-it-workers", - "Governance": "governance", - "DevSecOps": "devsecops", - "Isolation & Sandboxing": "devsecops", + "Infrastructure": "infrastructure", + "Domain & DNS Security": "infrastructure", + "Monitoring": "monitoring", + "Multisig for Protocols": "multisig-for-protocols", + "Multisig Administration": "multisig-for-protocols", + "Operational Runbooks": "multisig-for-protocols", + "For Signers": "multisig-for-protocols", + "Operational Security": "opsec", + "OpSec Core Concepts": "opsec", + "While Traveling": "opsec", "Privacy": "privacy", - "Supply Chain": "supply-chain", - "Security Automation": "security-automation", - "Identity and Access Management IAM": "iam", + "Safe Harbor": "safe-harbor", "Secure Software Development": "secure-software-development", + "Security Automation": "security-automation", "Security Testing": "security-testing", - "AI Security": "ai-security", - "ENS": "ens", - "Safe Harbor": "safe-harbor", - "Encryption": "encryption", + "Supply Chain": "supply-chain", + "Threat Modeling": "threat-modeling", "Treasury Operations": "treasury-operations", + "Vulnerability Disclosure": "vulnerability-disclosure", + "Wallet Security": "wallet-security", + "Signing & Verification": "wallet-security", "Guides": "guides", "Account Management": "guides", "Endpoint Security": "guides",