Menu

Change language

GitHub repo

Integrations — Logo Grid

blockmarketing

Integration marquee grid: category chips filter a wall of square logo tiles with hover labels, over a count line and a request-an-integration prompt.

Installation

$npx uipkge-ng@latest add integrations-logo-grid
Manifest:https://uipkge.dev/r/angular/integrations-logo-grid.jsonInstalls to:src/app/components/blocks/integrations-logo-grid/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

LogoIntegration
interface LogoIntegration {
  name: string
  mark: string
  category: string
}

Files installed (2)

  • src/app/components/blocks/integrations-logo-grid/integrations-logo-grid.component.ts5.7 kB
    // Integration marquee grid: category chips filter a wall of square logo tiles
    // with hover labels, over a count line and a request-an-integration prompt.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { ArrowRight, LucideAngularModule } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiToggleGroupComponent, UiToggleGroupItemComponent } from '@/ui/toggle-group/toggle-group.component'
    
    interface LogoIntegration {
      name: string
      mark: string
      category: string
    }
    
    // Monogram stand-ins: swap the `mark` for a real logo file per integration.
    const integrations: LogoIntegration[] = [
      { name: 'Snowflake', mark: 'SF', category: 'Warehouses' },
      { name: 'BigQuery', mark: 'BQ', category: 'Warehouses' },
      { name: 'Redshift', mark: 'RS', category: 'Warehouses' },
      { name: 'Databricks', mark: 'DB', category: 'Warehouses' },
      { name: 'Postgres', mark: 'PG', category: 'Warehouses' },
      { name: 'Okta', mark: 'OK', category: 'Identity' },
      { name: 'Entra ID', mark: 'EI', category: 'Identity' },
      { name: 'Google Workspace', mark: 'GW', category: 'Identity' },
      { name: 'Slack', mark: 'SL', category: 'Alerting' },
      { name: 'PagerDuty', mark: 'PD', category: 'Alerting' },
      { name: 'Opsgenie', mark: 'OG', category: 'Alerting' },
      { name: 'dbt', mark: 'DT', category: 'Modelling' },
      { name: 'Airflow', mark: 'AF', category: 'Modelling' },
      { name: 'Dagster', mark: 'DG', category: 'Modelling' },
      { name: 'Fivetran', mark: 'FT', category: 'Modelling' },
      { name: 'Looker', mark: 'LK', category: 'Downstream' },
      { name: 'Hex', mark: 'HX', category: 'Downstream' },
      { name: 'Sigma', mark: 'SG', category: 'Downstream' },
    ]
    
    const ALL = 'All'
    const categories: string[] = [ALL, ...new Set(integrations.map((i) => i.category))]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-integrations-logo-grid, [ui-integrations-logo-grid]',
      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: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiToggleGroupComponent,
        UiToggleGroupItemComponent,
      ],
      template: `
        <section data-slot="integrations-logo-grid" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
            <div class="flex flex-wrap items-end justify-between gap-6">
              <div class="max-w-2xl">
                <span ui-badge variant="secondary">Integrations</span>
                <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Works with what you already run</h2>
                <p class="text-muted-foreground mt-3 text-lg">
                  Read-only connections, configured once. Nothing here needs an agent installed on your side.
                </p>
              </div>
    
              <div
                ui-toggle-group
                type="single"
                variant="outline"
                size="sm"
                class="flex-nowrap overflow-x-auto"
                aria-label="Filter integrations by category"
                [value]="active()"
                (valueChange)="onCategory($event)"
              >
                @for (category of allCategories; track category) {
                  <button ui-toggle-group-item [value]="category">{{ category }}</button>
                }
              </div>
            </div>
    
            <div
              class="bg-border border-border mt-10 grid grid-cols-2 gap-px overflow-hidden rounded-xl border sm:grid-cols-4 lg:grid-cols-6"
            >
              @for (integration of visible(); track integration.name) {
                <div
                  class="bg-background hover:bg-muted/60 group flex aspect-square flex-col items-center justify-center gap-2 p-4 transition-colors"
                >
                  <span
                    class="border-border text-muted-foreground group-hover:text-foreground flex size-11 items-center justify-center rounded-lg border font-mono text-sm font-semibold transition-colors"
                    aria-hidden="true"
                  >
                    {{ integration.mark }}
                  </span>
                  <span class="text-muted-foreground group-hover:text-foreground text-center text-xs transition-colors">
                    {{ integration.name }}
                  </span>
                </div>
              }
            </div>
    
            <div class="border-border mt-6 flex flex-wrap items-center justify-between gap-4 border-t pt-5">
              <p class="text-muted-foreground text-sm">
                {{ visible().length }} of {{ total }} integrations shown · more added every release.
              </p>
              <button ui-button variant="ghost" size="sm">
                Request an integration
                <lucide-icon [img]="ArrowRight" class="ml-1.5 size-3.5" aria-hidden="true" />
              </button>
            </div>
          </div>
        </section>
      `,
    })
    export class UiIntegrationsLogoGridComponent {
      protected readonly ArrowRight = ArrowRight
    
      protected readonly allCategories = categories
      protected readonly total = integrations.length
    
      @Input('class') className?: string
    
      readonly active = signal<string>(ALL)
    
      readonly visible = computed(() => {
        const current = this.active()
        return current === ALL ? integrations : integrations.filter((i) => i.category === current)
      })
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      onCategory(value: string | string[]): void {
        const next = Array.isArray(value) ? value[0] : value
        // Single-select emits '' when the pressed item is pressed again;
        // fall back to ALL so the wall is never empty.
        this.active.set(next || ALL)
      }
    }
    
  • src/app/components/blocks/integrations-logo-grid/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/integrations-logo-grid.json