private/55-advanced.ps1
|
# UI4ME 2.1 — additional high-value, Python-parity presentation and input controls. # This file is dot-sourced by Ui4me.psm1. No compiled code, JS or Python runtime. function New-UI4MEShell { <# .SYNOPSIS Semantic desktop application shell; place New-UI4MESidebar/Content/Header inside. #> [CmdletBinding()] param([Parameter(Mandatory)][scriptblock]$Content) return (Invoke-UiContainer 'div' 'u4-shell' '' $Content) } function New-UI4MESidebar { [CmdletBinding()] param([Parameter(Mandatory)][scriptblock]$Content, [string]$Title) $prefix = if ($Title) { '<h3 class="u4-sidebar-title">{0}</h3>' -f (ConvertTo-UiHtml $Title) } else { '' } return (Invoke-UiContainer 'aside' 'u4-sidebar u4-column u4-gap-sm' '' $Content $null $null $prefix) } function New-UI4MEContent { [CmdletBinding()] param([Parameter(Mandatory)][scriptblock]$Content) return (Invoke-UiContainer 'section' 'u4-content u4-column u4-gap-md' '' $Content) } function New-UI4MEHeader { [CmdletBinding()] param([Parameter(Mandatory)][scriptblock]$Content) return (Invoke-UiContainer 'header' 'u4-header u4-row u4-align-center u4-gap-md' '' $Content) } function New-UI4MEFooter { [CmdletBinding()] param([Parameter(Mandatory)][scriptblock]$Content) return (Invoke-UiContainer 'footer' 'u4-footer u4-row u4-gap-sm' '' $Content) } function New-UI4MEStatusBar { [CmdletBinding()] param([Parameter(Mandatory)][scriptblock]$Content) return (Invoke-UiContainer 'div' 'u4-statusbar u4-row u4-align-center' '' $Content $null @{ role = 'status' }) } function New-UI4MEFieldset { <# .SYNOPSIS Group related fields under an accessible legend. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Legend, [Parameter(Mandatory)][scriptblock]$Content) if (-not $Legend.Trim()) { throw 'Fieldset legend is required' } $prefix = '<legend class="u4-fieldset-legend">{0}</legend>' -f (ConvertTo-UiHtml $Legend) return (Invoke-UiContainer 'fieldset' 'u4-fieldset u4-column u4-gap-sm' '' $Content $null $null $prefix) } function New-UI4MEMetric { <# .SYNOPSIS Accessible reactive statistic / KPI card; -Value can be a state. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Label, [AllowNull()][object]$Value, [string]$Detail = '') $id = New-UiId 'metric' $initial = Get-UiValue $Value Add-UiMarkup ('<section class="u4-metric" aria-label="{0}"><span class="u4-metric-label">{0}</span><strong id="{1}" class="u4-metric-value">{2}</strong><small class="u4-metric-detail">{3}</small></section>' -f (ConvertTo-UiHtml $Label), $id, (ConvertTo-UiHtml $initial), (ConvertTo-UiHtml $Detail)) if (Test-UiState $Value) { $null = Register-UiBinding $Value $id 'Set-UiTextBinding' } return $id } function New-UI4MEEmpty { <# .SYNOPSIS Accessible empty state; -Action can display a button with a callback. #> [CmdletBinding()] param([string]$Title = 'Nothing here yet', [string]$Description = '', [string]$ActionLabel, [scriptblock]$OnAction) $id = New-UiId 'empty' Add-UiMarkup ('<section id="{0}" class="u4-state u4-empty"><h3>{1}</h3><p>{2}</p>' -f $id, (ConvertTo-UiHtml $Title), (ConvertTo-UiHtml $Description)) if ($ActionLabel -and $OnAction) { $null = New-UI4MEButton -Text $ActionLabel -OnClick $OnAction } Add-UiMarkup '</section>' return $id } function New-UI4MELoading { <# .SYNOPSIS A status region with a spinner and an accessible loading message. #> [CmdletBinding()] param([string]$Text = 'Loading...') $id = New-UiId 'loading' Add-UiMarkup ('<div id="{0}" class="u4-state u4-loading" role="status"><span class="u4-spinner" aria-hidden="true"></span><span>{1}</span></div>' -f $id, (ConvertTo-UiHtml $Text)) return $id } $script:UiIconPaths = @{ plus='M12 5v14M5 12h14'; minus='M5 12h14'; check='M20 6 9 17l-5-5' x='M18 6 6 18M6 6l12 12'; search='m21 21-4.35-4.35M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16' menu='M4 7h16M4 12h16M4 17h16'; 'chevron-down'='m6 9 6 6 6-6' 'chevron-right'='m9 18 6-6-6-6'; folder='M3 6h6l2 3h10v11H3V6' home='m3 10 9-7 9 7v10H3V10m6 10v-7h6v7' refresh='M20 11a8 8 0 1 0-2 6m2-5v-6m0 6h-6' info='M12 17v-5m0-5h.01M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20' warning='m12 2 10 19H2L12 2m0 7v5m0 4h.01' download='M12 3v12m-5-5 5 5 5-5M4 17v4h16v-4' settings='M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M12 2v2M12 20v2M2 12h2M20 12h2' } function Get-UiSvgIcon { param([Parameter(Mandatory)][string]$Icon, [int]$Size = 18, [string]$Label) if (-not $script:UiIconPaths.ContainsKey($Icon)) { throw "Unknown UI4ME icon '$Icon'" } if ($Size -lt 8 -or $Size -gt 128) { throw 'Icon size must be between 8 and 128 px' } $attributes = if ($Label) { 'role="img" aria-label="{0}"' -f (ConvertTo-UiHtml $Label) } else { 'role="presentation" aria-hidden="true"' } return ('<svg class="u4-icon" viewBox="0 0 24 24" width="{0}" height="{0}" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" {1}><path d="{2}"></path></svg>' -f $Size, $attributes, $script:UiIconPaths[$Icon]) } function New-UI4MEIcon { <# .SYNOPSIS Inline SVG icon, no external font or network requests. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Name, [ValidateRange(8, 128)][int]$Size = 18, [string]$Label) $id = New-UiId 'icon' Add-UiMarkup ('<span id="{0}" class="u4-inline-icon">{1}</span>' -f $id, (Get-UiSvgIcon $Name $Size $Label)) return $id } function New-UI4MEIconButton { <# .SYNOPSIS Icon-only button; -Label is mandatory for accessibility. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Icon, [Parameter(Mandatory)][string]$Label, [scriptblock]$OnClick, [ValidateSet('primary', 'secondary', 'outline', 'ghost', 'danger')][string]$Variant = 'ghost', [AllowNull()][object]$Disabled = $false) if (-not $Label.Trim()) { throw 'An icon button needs a nonempty accessible -Label' } $id = New-UiId 'iconbutton' $disabledAttr = if ([bool](Get-UiValue $Disabled)) { ' disabled' } else { '' } Add-UiMarkup ('<button type="button" class="u4-btn u4-btn-{0} u4-icon-button" id="{1}" aria-label="{2}"{3}>{4}</button>' -f $Variant, $id, (ConvertTo-UiHtml $Label), $disabledAttr, (Get-UiSvgIcon $Icon 18 $null)) if ($OnClick) { $null = Register-UiHandler $id 'click' $OnClick } Register-UiDisabled $id $Disabled return $id } function New-UI4METooltip { <# .SYNOPSIS CSS hover and focus tooltip for an included focusable trigger. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Text, [Parameter(Mandatory)][scriptblock]$Content) if (-not $Text.Trim()) { throw 'Tooltip text must not be empty' } $id = New-UiId 'tip' # The trigger region is described by the tip, so assistive technology announces it. Add-UiMarkup ('<span class="u4-tooltip-root" id="{0}"><span class="u4-tooltip-trigger" aria-describedby="{0}-text">' -f $id) try { Invoke-UiBuild $Content } finally { Add-UiMarkup ('</span><span role="tooltip" id="{0}-text" class="u4-tooltip-text">{1}</span></span>' -f $id, (ConvertTo-UiHtml $Text)) } return $id } function New-UI4MEBreadcrumbs { <# .SYNOPSIS Readable path navigation label; items are displayed as text, not unsafe raw links. #> [CmdletBinding()] param([Parameter(Mandatory)][string[]]$Items) $id = New-UiId 'crumbs' Add-UiMarkup ('<nav id="{0}" aria-label="Breadcrumb"><ol class="u4-breadcrumbs">' -f $id) $index = 0 foreach ($label in $Items) { $index++ $current = if ($index -eq $Items.Count) { ' aria-current="page"' } else { '' } Add-UiMarkup ('<li{0}>{1}</li>' -f $current, (ConvertTo-UiHtml $label)) } Add-UiMarkup '</ol></nav>' return $id } function New-UI4MESegmented { <# .SYNOPSIS Accessible horizontal radio group styled as segmented choices. #> [CmdletBinding()] param([string]$Label = 'Choose', [Parameter(Mandatory)][object[]]$Options, [AllowNull()][object]$Bind, [scriptblock]$OnChange) return (New-UI4MERadio -Label $Label -Options $Options -Bind $Bind -OnChange $OnChange -Horizontal -Class 'u4-segmented') } function New-UI4MEMultiSelect { <# .SYNOPSIS A fieldset of labelled checkboxes with state holding an array of selected values. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Label, [Parameter(Mandatory)][object[]]$Options, [AllowNull()][object]$Bind, [object[]]$Values = @(), [scriptblock]$OnChange) if ($null -ne $Bind -and -not (Test-UiState $Bind)) { throw '-Bind expects New-UI4MEState' } $optionsData = ConvertTo-UiOptions $Options $initial = if ($null -ne $Bind) { @(Get-UiValue $Bind) } else { @($Values) } $id = New-UiId 'multi' $map = @{}; $last = @{} Add-UiMarkup ('<fieldset class="u4-fieldset u4-multiselect" id="{0}"><legend class="u4-fieldset-legend">{1}</legend><div class="u4-multiselect-options">' -f $id, (ConvertTo-UiHtml $Label)) $index = 0 foreach ($option in $optionsData) { $index++ $eid = "$id-$index" $map[$eid] = $option.Value $selected = @($initial) -contains $option.Value $last[$eid] = $selected $checked = if ($selected) { ' checked' } else { '' } Add-UiMarkup ('<label class="u4-check"><input type="checkbox" id="{0}"{1}><span>{2}</span></label>' -f $eid, $checked, (ConvertTo-UiHtml $option.Label)) } Add-UiMarkup '</div></fieldset>' $state = if ($null -ne $Bind) { $Bind } else { New-UI4MEState -Value @($initial) } $data = [pscustomobject]@{ Map = $map; Last = $last; State = $state; Callback = $OnChange } $null = Register-UiBinding $state $id 'Set-UiMultiSelectBinding' $data foreach ($eid in $map.Keys) { $handler = New-UiCallable 'Invoke-UiMultiSelectEvent' ([pscustomobject]@{ Id = $eid; Data = $data }) $null = Register-UiHandler $eid 'input' $handler $null = Register-UiHandler $eid 'change' $handler } return $id } function Set-UiMultiSelectBinding { param($ctx, $Element, $Value, $Data) $selected = @($Value) foreach ($id in $Data.Map.Keys) { $checked = $selected -contains $Data.Map[$id] if ($Data.Last[$id] -eq $checked) { continue } $Data.Last[$id] = $checked $null = Set-UI4MEChecked -Document $ctx.Document -Element $id -Checked $checked } } function Invoke-UiMultiSelectEvent { param($Item, $Event) $data = $Item.Data $checked = [bool](Get-UI4MEChecked -Document $script:UiBuilder.Document -Element $Item.Id) if ($data.Last[$Item.Id] -eq $checked) { return } $data.Last[$Item.Id] = $checked $value = $data.Map[$Item.Id] $selected = @($data.State.Value | Where-Object { $_ -ne $value }) if ($checked) { $selected += $value } $data.State.Set(@($selected)) if ($data.Callback) { $null = & $data.Callback @($selected) } } function New-UI4MEResizableSplit { <# .SYNOPSIS Two responsive panes; a native range input adjusts the width via CSS. #> [CmdletBinding()] param([Parameter(Mandatory)][scriptblock]$Side, [Parameter(Mandatory)][scriptblock]$Main, [ValidateRange(15, 85)][int]$Percent = 35, [string]$Label = 'Resize panels') $id = New-UiId 'split' Add-UiMarkup ('<section class="u4-adjustable"><div class="u4-adjustable-controls"><label for="{0}-slider">{1}</label><input id="{0}-slider" type="range" min="15" max="85" value="{2}" aria-label="{1}"></div><div class="u4-resizable-panes" id="{0}" style="--u4-split:{2}%"><div class="u4-resizable-side">' -f $id, (ConvertTo-UiHtml $Label), $Percent) try { Invoke-UiBuild $Side } finally { Add-UiMarkup '</div><div class="u4-resizable-main">' } try { Invoke-UiBuild $Main } finally { Add-UiMarkup '</div></div></section>' } $data = [pscustomobject]@{ Id = $id } $null = Register-UiHandler "$id-slider" 'input' (New-UiCallable 'Invoke-UiResizeSplit' $data) return $id } function Invoke-UiResizeSplit { param($Data, $Event) $n = 35 if (-not [int]::TryParse([string]$Event.value, [ref]$n)) { return } $n = [Math]::Max(15, [Math]::Min(85, $n)) $null = Set-UI4MEDocumentAttribute -Document $script:UiBuilder.Document -Element $Data.Id -Name style -Value ("--u4-split:$n%") } function New-UI4MELog { <# .SYNOPSIS A bounded accessible log view. Append from the app runspace with Add-UI4MELogLine. #> [CmdletBinding()] param([ValidateRange(1, 5000)][int]$MaxLines = 500, [string]$Label = 'Application log') $ctx = Assert-UiBuilder $id = New-UiId 'log' if (-not $ctx.PSObject.Properties['UiLogs']) { $ctx | Add-Member -NotePropertyName UiLogs -NotePropertyValue @{} } $ctx.UiLogs[$id] = [pscustomobject]@{ Max = $MaxLines; Lines = (New-Object Collections.Generic.List[string]) } if ($null -ne $ctx.CurrentOwner) { [void]$ctx.CurrentOwner.Cleanups.Add((New-UiCallable 'Remove-UiLog' $id)) } Add-UiMarkup ('<div id="{0}" class="u4-log" role="log" aria-label="{1}" aria-live="polite" aria-relevant="additions text"></div>' -f $id, (ConvertTo-UiHtml $Label)) return $id } function Remove-UiLog { param($Id) $ctx = $script:UiBuilder; if ($null -ne $ctx -and $ctx.PSObject.Properties['UiLogs']) { [void]$ctx.UiLogs.Remove($Id) } } function Add-UI4MELogLine { [CmdletBinding()] param([Parameter(Mandatory)][string]$Element, [Parameter(Mandatory)][AllowEmptyString()][string]$Text, [ValidateSet('info', 'success', 'warning', 'danger')][string]$Level = 'info') $ctx = Assert-UiBuilder if (-not $ctx.PSObject.Properties['UiLogs'] -or -not $ctx.UiLogs.ContainsKey($Element)) { throw "Unknown UI4ME log '$Element'" } $log = $ctx.UiLogs[$Element] $line = '<div class="u4-log-line u4-log-{0}">{1}</div>' -f $Level, (ConvertTo-UiHtml $Text) $log.Lines.Add($line) if ($log.Lines.Count -gt $log.Max) { $log.Lines.RemoveAt(0) } if ($ctx.Document) { $null = Set-UI4MEDocumentHtml -Document $ctx.Document -Element $Element -Html ($log.Lines -join '') } } function New-UI4MECopyButton { <# .SYNOPSIS Copy plain text through the native Core clipboard; needs user interaction on Wayland. #> [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$Text, [string]$Label = 'Copy') $data = [pscustomobject]@{ Text = $Text } $id = New-UiId 'copy' Add-UiMarkup ('<button id="{0}" type="button" class="u4-btn u4-btn-secondary">{1}</button>' -f $id, (ConvertTo-UiHtml $Label)) $null = Register-UiHandler $id 'click' (New-UiCallable 'Invoke-UiCopyButton' $data) return $id } function Invoke-UiCopyButton { param($Data, $Event) $ctx = Assert-UiBuilder $null = Set-UI4MEClipboard -Application $ctx.App -Text ([string]$Data.Text) } |