Public/Add-DhFilterCard.ps1

function Add-DhFilterCard {
    <#
    .SYNOPSIS
        Add a clickable card grid that filters a target table.
 
    .DESCRIPTION
        Renders a row of clickable cards — each card represents a distinct value
        from a field in the target table. Clicking a card instantly filters the
        table to show only matching rows. An active filter banner appears below
        the grid with a Clear Filter button.
 
        The cards are independent of the table data: you supply them explicitly
        so they can include display labels, counts, icons, and sub-labels that
        differ from the raw field values.
 
        Use -MultiFilter to allow more than one card to be active simultaneously.
 
    .PARAMETER Report Dashboard object from New-DhDashboard.
    .PARAMETER Id Unique identifier (alphanumeric, - or _).
    .PARAMETER Title Section heading shown above the cards.
    .PARAMETER TargetTableId TableId of the table to filter.
    .PARAMETER FilterField Field name in the target table to match against.
    .PARAMETER Cards
        Array of card hashtables:
          @{
              Label = 'Card display name' # REQUIRED
              Value = 'match-value' # REQUIRED — matched against FilterField
              SubLabel = 'Secondary label' # optional
              Count = 42 # optional — badge top-right of card
              Icon = 'X' # optional
          }
 
    .PARAMETER MultiFilter Allow multiple cards active simultaneously ($false).
    .PARAMETER ShowCount Show count badge on cards (default $true).
 
    .EXAMPLE
        # Filter a table by a grouping field
        $locationCards = $servers | Group-Object Location | ForEach-Object {
            @{ Label = $_.Name; Value = $_.Name; Count = $_.Count }
        }
        Add-DhFilterCard -Report $report -Id 'loc-filter' `
            -Title 'Filter by Location' `
            -TargetTableId 'servers' -FilterField 'Location' `
            -Cards $locationCards
 
    .EXAMPLE
        # Multi-select filter (multiple cards can be active at once)
        Add-DhFilterCard -Report $report -Id 'env-filter' `
            -Title 'Filter by Environment' `
            -TargetTableId 'resources' -FilterField 'Environment' `
            -MultiFilter $true `
            -Cards @(
                @{ Label = 'Production'; Value = 'prod'; Count = 45 }
                @{ Label = 'Staging'; Value = 'stg'; Count = 12 }
                @{ Label = 'Development'; Value = 'dev'; Count = 30 }
            )
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [System.Collections.Specialized.OrderedDictionary] $Report,
        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z0-9_-]+$')]
        [string]   $Id,
        [Parameter(Mandatory)] [string]    $Title,
        [Parameter(Mandatory)] [string]    $TargetTableId,
        [Parameter(Mandatory)] [string]    $FilterField,
        [Parameter(Mandatory)] [object[]]  $Cards,
        [switch]   $MultiFilter,
        [bool]     $ShowCount   = $true,
        [bool]   $Collapsible = $true,    # wrap the block in a collapsible header (chevron + click-to-toggle)
        [bool]   $DefaultOpen = $true,    # whether the collapsible body starts expanded (only used when -Collapsible is $true)
        [ValidateRange(0,12)]
        [int]    $GridSpan    = 0,   # v1.10.0 - columns (1-12) to span in -report-grid layout; 0 = default flow
        [string] $NavGroup    = '',   # primary nav group label (enables two-tier nav)
        [string]   $NavSubGroup = '',    # optional second-level group under NavGroup (enables three-tier nav)
        # ---- v2.0.0 navigation (see readme-dev.md section 15) ----
        [string] $NavPath      = '',   # 'A/B/C' - where this item lives. '//' escapes a literal '/'
        [ValidateSet('','Exact','Subtree','Global')]
        [string] $NavScope     = '',   # Exact (default) | Subtree (this node and below) | Global (everywhere)
        [string] $NavLabel     = '',   # display text for this node, decoupled from the path key
        [int]    $NavOrder     = 0     # explicit sibling ordering; 0 = declaration order
    )

    # v2.0.0 - resolve nav declaration once, in one place. Throws if -NavPath is
    # combined with -NavGroup/-NavSubGroup rather than silently preferring one.
    $navDecl = Resolve-DhNavDeclaration -NavPath $NavPath -NavScope $NavScope `
                    -NavGroup $NavGroup -NavSubGroup $NavSubGroup `
                    -ItemKind 'Block' -Context 'Add-DhFilterCard'
    # Monotonic declaration number, shared by tables and blocks. The exporter
    # emits ALL blocks before ALL tables, so without this the menu would order
    # nodes by section type instead of by the order the author wrote them.
    if (-not $Report.Contains('_DhNavSeq')) { $Report['_DhNavSeq'] = 0 }
    $navSeqNo = [int]$Report['_DhNavSeq']; $Report['_DhNavSeq'] = $navSeqNo + 1

    if (-not $Report.Contains('Blocks')) {
        $Report['Blocks'] = [System.Collections.Generic.List[hashtable]]::new()
    }

    # Duplicate-Id guard (same rule applied by other block cmdlets)
    foreach ($existing in $Report.Blocks) {
        if ($existing.Id -eq $Id) {
            throw "Add-DhFilterCard: A block with Id '$Id' already exists in this report. Use a unique Id."
        }
    }

    if ($Report.Tables -and -not ($Report.Tables | Where-Object { $_.Id -eq $TargetTableId })) {
        throw "Add-DhFilterCard: Target table '$TargetTableId' not found. Add the table before the filter grid."
    }

    $normCards = foreach ($card in $Cards) {
        $c = @{} + $card
        if (-not $c.Contains('SubLabel')) { $c['SubLabel'] = '' }
        if (-not $c.Contains('Count'))    { $c['Count']    = $null }
        if (-not $c.Contains('Icon'))     { $c['Icon']     = '' }
        $c
    }

    $Report.Blocks.Add([ordered]@{
        BlockType     = 'filtercardgrid'
        Id            = $Id
        Title         = $Title
        TargetTableId = $TargetTableId
        FilterField   = $FilterField
        Cards         = @($normCards)
        MultiFilter   = $MultiFilter
        ShowCount     = $ShowCount
        NavGroup      = $NavGroup
        NavSubGroup = $NavSubGroup
        NavPath     = $NavPath
        NavSegments     = $navDecl.Path
        NavSeqNo     = $navSeqNo
        NavScope    = $navDecl.Scope
        NavLabel    = $NavLabel
        NavOrder    = $NavOrder
        GridSpan    = $GridSpan
        Collapsible = $Collapsible
        DefaultOpen = $DefaultOpen
    })
    Write-Verbose "Add-DhFilterCard: '$Id' -> table '$TargetTableId' on field '$FilterField' ($(@($normCards).Count) cards)."
}