Team Section
blockmarketingCentered marketing team section. Eyebrow + headline + lede over a responsive member card grid (initials avatar, role badge, one-line bio, ghost social buttons). Members with a department field group under uppercase headers; variant="compact" drops bios for tighter cards. Roster is stub data — edit DEFAULT_MEMBERS or pass members.
Installation
$pnpm dlx uipkge-ng@latest add team-section$npx uipkge-ng@latest add team-section$yarn dlx uipkge-ng@latest add team-section$bunx uipkge-ng@latest add team-sectionManifest:
https://uipkge.dev/r/angular/team-section.jsonInstalls to:src/app/components/blocks/team-section/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
TeamMemberinterface TeamMember {
name: string
role: string
bio?: string
/** When any member has a department, the grid groups under uppercase headers. */
department?: string
twitter?: string
github?: string
linkedin?: string
}MemberSectioninterface MemberSection {
department?: string
members: TeamMember[]
}npm dependencies
Files installed (2)
src/app/components/blocks/team-section/team-section.component.ts10.5 kB
// Marketing team section: eyebrow + headline + lede over a responsive member card // grid (initials avatar, role badge, one-line bio, ghost social buttons). Members // with a department field group under uppercase headers; variant="compact" drops // bios for tighter cards. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' export interface TeamMember { name: string role: string bio?: string /** When any member has a department, the grid groups under uppercase headers. */ department?: string twitter?: string github?: string linkedin?: string } /** Stub roster — swap for your data source after installing, or pass `members`. */ const DEFAULT_MEMBERS: TeamMember[] = [ { name: 'Amara Okafor', role: 'Co-founder & CEO', bio: 'Sets the vision and still reviews every release note before it ships.', twitter: 'https://x.com/amaraokafor', linkedin: 'https://linkedin.com/in/amaraokafor', }, { name: 'Jonas Lindqvist', role: 'Co-founder & CTO', bio: 'Owns architecture and keeps p95 latency under 80ms in every region.', github: 'https://github.com/jonaslindqvist', linkedin: 'https://linkedin.com/in/jonaslindqvist', }, { name: 'Priya Raghavan', role: 'Staff Engineer', bio: 'Leads the platform team and maintains our public REST and GraphQL APIs.', github: 'https://github.com/priyaraghavan', }, { name: 'Marco Deluca', role: 'Product Designer', bio: 'Turns messy workflows into calm interfaces and runs our weekly design critique.', twitter: 'https://x.com/marcodeluca', github: 'https://github.com/marcodeluca', linkedin: 'https://linkedin.com/in/marcodeluca', }, { name: 'Tomoko Saito', role: 'Head of People', bio: 'Builds hiring loops that respect candidates and onboarding that actually sticks.', linkedin: 'https://linkedin.com/in/tomokosaito', }, { name: 'Felix Adeyemi', role: 'Growth Engineer', bio: 'Experiments across onboarding and pricing pages; ships behind flags daily.', twitter: 'https://x.com/felixadeyemi', }, ] export interface MemberSection { department?: string members: TeamMember[] } export function initialsFor(name: string): string { return name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((part) => part.charAt(0).toUpperCase()) .join('') } export function getSections(members: TeamMember[]): MemberSection[] { if (!members.some((member) => member.department)) return [{ members }] const grouped = new Map<string, TeamMember[]>() for (const member of members) { const key = member.department ?? '' if (!grouped.has(key)) grouped.set(key, []) grouped.get(key)!.push(member) } return Array.from(grouped.entries()).map(([department, groupedMembers]) => ({ department: department || undefined, members: groupedMembers, })) } const X_SVG_PATH = 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' const GITHUB_SVG_PATH = 'M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.15c-3.2.69-3.87-1.36-3.87-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.02 1.76 2.69 1.25 3.35.96.1-.75.4-1.25.72-1.54-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11.1 11.1 0 0 1 5.78 0c2.21-1.49 3.18-1.18 3.18-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.83 1.19 3.09 0 4.41-2.69 5.38-5.26 5.66.41.35.78 1.05.78 2.12v3.14c0 .31.21.67.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5z' const LINKEDIN_SVG_PATH = 'M20.45 20.45h-3.55v-5.57c0-1.33-.03-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.36V9h3.41v1.56h.05c.47-.9 1.63-1.85 3.36-1.85 3.6 0 4.27 2.37 4.27 5.46zM5.34 7.43a2.06 2.06 0 1 1 0-4.12 2.06 2.06 0 0 1 0 4.12zM7.12 20.45H3.56V9h3.56zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.55C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.72C24 .77 23.2 0 22.22 0z' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-team-section, [ui-team-section]', standalone: true, // React renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, ], template: ` <section data-slot="team-section" [class]="rootClass"> <div class="mx-auto max-w-6xl px-6 py-24"> @if (showHeader) { <div data-slot="team-section-header" class="mx-auto max-w-2xl text-center"> @if (eyebrow) { <p class="text-primary text-sm font-medium tracking-widest uppercase">{{ eyebrow }}</p> } @if (title) { <h2 class="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">{{ title }}</h2> } @if (description) { <p class="text-muted-foreground mt-3 text-lg leading-relaxed">{{ description }}</p> } </div> } @for (section of sections; track section.department ?? 'team'; let i = $index) { <div data-slot="team-section-group" [class]="i > 0 || showHeader ? 'mt-12' : undefined"> @if (section.department) { <h3 class="text-muted-foreground text-xs font-semibold tracking-widest uppercase"> {{ section.department }} </h3> } <div data-slot="team-section-grid" [class]="gridClass(section.department)"> @for (member of section.members; track member.name) { <div ui-card class="text-center"> <div ui-card-content [class]="cardContentClass"> <ui-avatar [class]="compact ? 'size-10' : 'size-16'"> <ui-avatar-fallback [class]="compact ? 'text-sm' : 'text-lg'"> {{ initialsFor(member.name) }} </ui-avatar-fallback> </ui-avatar> <div class="space-y-1"> <p class="text-sm font-semibold">{{ member.name }}</p> <span ui-badge variant="secondary">{{ member.role }}</span> @if (!compact && member.bio) { <p class="text-muted-foreground line-clamp-2 text-xs leading-relaxed">{{ member.bio }}</p> } </div> @if (member.twitter || member.github || member.linkedin) { <div class="flex items-center gap-0.5"> @if (member.twitter) { <a ui-button variant="ghost" size="icon-sm" [href]="member.twitter" target="_blank" rel="noreferrer" [attr.aria-label]="member.name + ' on X (Twitter)'" > <svg viewBox="0 0 24 24" fill="currentColor" class="size-4" aria-hidden="true"> <path [attr.d]="xSvgPath" /> </svg> </a> } @if (member.github) { <a ui-button variant="ghost" size="icon-sm" [href]="member.github" target="_blank" rel="noreferrer" [attr.aria-label]="member.name + ' on GitHub'" > <svg viewBox="0 0 24 24" fill="currentColor" class="size-4" aria-hidden="true"> <path [attr.d]="githubSvgPath" /> </svg> </a> } @if (member.linkedin) { <a ui-button variant="ghost" size="icon-sm" [href]="member.linkedin" target="_blank" rel="noreferrer" [attr.aria-label]="member.name + ' on LinkedIn'" > <svg viewBox="0 0 24 24" fill="currentColor" class="size-4" aria-hidden="true"> <path [attr.d]="linkedinSvgPath" /> </svg> </a> } </div> } </div> </div> } </div> </div> } </div> </section> `, }) export class UiTeamSectionComponent { @Input() eyebrow = 'Our team' @Input() title = 'The people behind the product' @Input() description = 'A small team with strong opinions about craft. We design, build and support everything ourselves — no outsourcing, no black boxes.' @Input() members: TeamMember[] = DEFAULT_MEMBERS /** 'grid' shows full cards with bios; 'compact' tightens cards and drops them. */ @Input() variant: 'grid' | 'compact' = 'grid' @Input('class') className?: string protected readonly xSvgPath = X_SVG_PATH protected readonly githubSvgPath = GITHUB_SVG_PATH protected readonly linkedinSvgPath = LINKEDIN_SVG_PATH get compact(): boolean { return this.variant === 'compact' } get showHeader(): boolean { return Boolean(this.eyebrow || this.title || this.description) } get sections(): MemberSection[] { return getSections(this.members ?? DEFAULT_MEMBERS) } get rootClass(): string { return cn('bg-background', this.className) } get cardContentClass(): string { return cn('flex flex-col items-center', this.compact ? 'gap-3 p-4' : 'gap-4 p-6') } gridClass(department: string | undefined): string { return cn('grid gap-6 sm:grid-cols-2 lg:grid-cols-3', this.compact && 'gap-4', department && 'mt-6') } initialsFor(name: string): string { return initialsFor(name) } }src/app/components/blocks/team-section/index.ts0.1 kB
export { UiTeamSectionComponent, getSections, initialsFor, type MemberSection, type TeamMember, } from './team-section.component'
Raw manifest:https://uipkge.dev/r/angular/team-section.json