Public/Add-DhCollapsible.ps1

function Add-DhCollapsible {
    <#
    .SYNOPSIS
        Add a collapsible section containing metadata cards or free-form content.

    .DESCRIPTION
        Renders a clickable header bar with a chevron toggle and an optional item
        count badge. The body expands or collapses when the header is clicked.

        Two content modes:
          -Cards A responsive grid of key-value cards (accounts, environments,
                    locations, servers — any structured metadata)
          -Content Free-form HTML rendered inside the collapsible body

    .PARAMETER Report Dashboard object from New-DhDashboard.
    .PARAMETER Id Unique identifier (alphanumeric, - or _).
    .PARAMETER Title Header text shown in the toggle bar.
    .PARAMETER Icon Optional emoji / unicode icon before the title.
    .PARAMETER DefaultOpen Start expanded (default $true).

    .PARAMETER Cards
        Array of card hashtables. Each card:
          @{
              Title = 'Card heading' # REQUIRED
              Fields = @( # REQUIRED — key-value pairs
                  @{ Label = 'ID'; Value = 'abc-123' }
                  @{ Label = 'Status'; Value = 'Active'; Class = 'cell-ok' }
              )
              Badge = 'Active' # optional badge text
              BadgeClass = 'cell-ok' # optional badge colour class
          }

    .PARAMETER Content
        Free-form HTML string (alternative to -Cards).

    .EXAMPLE
        # Card grid — any structured metadata (accounts, locations, teams…)
        Add-DhCollapsible -Report $report -Id 'accounts' `
            -Title 'Accounts' -Icon 'X' -DefaultOpen $true `
            -Cards @(
                @{
                    Title = 'Production Account'
                    Badge = 'Active'
                    BadgeClass = 'cell-ok'
                    Fields = @(
                        @{ Label = 'Account ID'; Value = 'acc-001-prod' }
                        @{ Label = 'Region'; Value = 'us-east-1' }
                        @{ Label = 'Resources'; Value = '147' }
                    )
                }
                @{
                    Title = 'DR Account'
                    Badge = 'Standby'
                    BadgeClass = 'cell-warn'
                    Fields = @(
                        @{ Label = 'Account ID'; Value = 'acc-002-dr' }
                        @{ Label = 'Region'; Value = 'us-west-2' }
                        @{ Label = 'Resources'; Value = '23'; Class = 'cell-warn' }
                    )
                }
            )

    .EXAMPLE
        # Free-form collapsible notes
        Add-DhCollapsible -Report $report -Id 'notes' `
            -Title 'Maintenance Notes' -DefaultOpen $false `
            -Content '<p>Last maintenance window: 2026-03-01. Next: 2026-04-01.</p>'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [System.Collections.Specialized.OrderedDictionary] $Report,
        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z0-9_-]+$')]
        [string]   $Id,
        [Parameter(Mandatory)] [string]    $Title,
        [string]   $Icon        = '',
        [bool]     $DefaultOpen = $true,
        [object[]] $Cards       = @(),
        [string]   $Content     = '',
        [string]   $NavGroup    = ''    # primary nav group label (enables two-tier nav)
    )

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

    $normCards = foreach ($card in $Cards) {
        if ($card -isnot [hashtable] -and $card -isnot [System.Collections.Specialized.OrderedDictionary]) {
            throw "Add-DhCollapsible: Each card must be a hashtable. Got: $($card.GetType().Name)"
        }
        if (-not $card.Contains('Title') -or $null -eq $card.Title) {
            throw "Add-DhCollapsible: Each card must have a 'Title' key."
        }
        if (-not $card.Contains('Fields')) {
            throw "Add-DhCollapsible: Each card must have a 'Fields' key (array of @{Label;Value} hashtables)."
        }
        # Normalize fields — ensure each field is a hashtable with Label, Value, Class keys
        $normFields = @($card.Fields) | ForEach-Object {
            $f = if ($_ -is [hashtable] -or $_ -is [System.Collections.Specialized.OrderedDictionary]) {
                @{} + $_
            } else {
                @{ Label = [string]$_.Label; Value = [string]$_.Value }
            }
            if (-not $f.Contains('Class')) { $f['Class'] = '' }
            $f
        }
        $c = @{} + $card
        $c['Fields'] = @($normFields)
        if (-not $c.Contains('Badge'))      { $c['Badge']      = '' }
        if (-not $c.Contains('BadgeClass')) { $c['BadgeClass'] = '' }
        $c
    }

    $Report.Blocks.Add([ordered]@{
        BlockType   = 'collapsible'
        Id          = $Id
        Title       = $Title
        Icon        = $Icon
        DefaultOpen = $DefaultOpen
        Cards       = @($normCards)
        Content     = $Content
        Badge       = @($normCards).Count
        NavGroup    = $NavGroup
    })
    Write-Verbose "Add-DhCollapsible: Added '$Id' ($(@($normCards).Count) cards)."
}