Phase 0: P1 accessibility/theming wave (#22) #23
No files matched your search
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"posthog@claude-plugins-official": true,
|
||||
"playwright@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
# Implementation plan: Accounts mode + workspace redesign
|
||||
|
||||
Delivers issue #16 (Accounts mode) built directly into the layout redesign
|
||||
direction recorded on that issue, with issue #22's findings reconciled along the
|
||||
way. Tracker of record: GitHub Issues; this document is the executable plan.
|
||||
|
||||
**Decisions already made (owner-confirmed on #16):** Windows Credential Manager
|
||||
for tokens · one account = one org · Sync All · CLI parity (`--account`).
|
||||
**Defaults standing unless overridden on #16:** NavigationView with Quick Sync
|
||||
pinned · Sync All runs accounts sequentially · accounts remember the full
|
||||
settings bundle and show a status badge · no account cap.
|
||||
|
||||
Every phase ends the same way: 0-warning build, all tests green, format clean,
|
||||
app launch verified, gated PR `dev -> main`, and a `v0.1.0-beta.N` tag so each
|
||||
increment is installable and self-update keeps getting exercised.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — P1 accessibility & theming wave (issue #22)
|
||||
|
||||
Prerequisite, not part of the redesign; everything here transfers unchanged
|
||||
into the new layout.
|
||||
|
||||
1. `StatusFormat.BrushFor` → resolve WinUI semantic brushes per call from
|
||||
`Application.Current.Resources` (`SystemFillColorSuccessBrush` Done,
|
||||
`SystemFillColorCriticalBrush` Failed, `SystemFillColorCautionBrush`
|
||||
Canceled, `SystemFillColorAttentionBrush` Cloning/Pulling, neutral Queued).
|
||||
Add `GlyphFor(SyncStatus)` returning Segoe Fluent Icons glyphs and a
|
||||
`FontIcon` beside the status text so state is never color-alone.
|
||||
2. `MainWindow.xaml` inline error: `Foreground="Crimson"` →
|
||||
`{ThemeResource SystemFillColorCriticalBrush}`.
|
||||
3. `AutomationProperties.LiveSetting="Polite"` on StatusText; raise UIA
|
||||
notifications for run summary and per-repo failures
|
||||
(`FrameworkElementAutomationPeer.RaiseNotificationEvent`).
|
||||
4. `AutomationProperties.Name` on per-row checkboxes (repo name), select-all,
|
||||
Resolve links (repo-specific), overall ProgressBar, both ProgressRings.
|
||||
|
||||
Verification: build/launch; Accessibility Insights or Narrator spot-check is an
|
||||
owner follow-up (headless agent cannot hear announcements). Tag `beta.5`.
|
||||
|
||||
## Phase 1 — Accounts foundation (libraries + CLI; no UI)
|
||||
|
||||
New in `gclo.ViewModels` (app-layer library, headless-testable):
|
||||
|
||||
- `Account` model: `Id` (guid), `Name` (unique, case-insensitive),
|
||||
`Description`, `Organization`, `TargetRoot`, `CreateOrgSubfolder`,
|
||||
`MaxConcurrency`, `LastSyncUtc`, `LastSyncSummary` (counts string).
|
||||
Metadata JSON at `%LOCALAPPDATA%\gclo\accounts.json` — never contains tokens.
|
||||
- `ITokenVault` { `Store(accountId, token)`, `TryRetrieve(accountId)`,
|
||||
`Delete(accountId)` } — implementation `CredentialManagerVault` using
|
||||
`Windows.Security.Credentials.PasswordVault`, resource name
|
||||
`gclo:account:<id>`; `InMemoryVault` fake for tests.
|
||||
- `AccountsStore`: CRUD + rename-safe persistence (same never-throw discipline
|
||||
as `AppSettings`); deleting an account deletes its vault entry.
|
||||
- CLI (`gclo.Cli`): `gclo accounts` (list: name, org, target, last sync);
|
||||
`gclo sync --account <name>` resolves registry + vault (error 2 if the vault
|
||||
entry is missing); explicit `--org/--token-*` flags override account values.
|
||||
`docs/CLI.md` section.
|
||||
|
||||
Tests: store CRUD/uniqueness/corrupt-file recovery; vault fake round-trip +
|
||||
delete-on-remove; CLI resolution and override precedence. The real PasswordVault
|
||||
is thin glue — verified by launch-time smoke, not unit tests. Tag `beta.6`.
|
||||
|
||||
## Phase 2 — Navigation shell
|
||||
|
||||
- `MainWindow` becomes a `NavigationView` (left pane): pinned "Quick Sync"
|
||||
item, accounts from `AccountsStore` (name + status dot + last-sync tooltip),
|
||||
footer "+ Add account". Content region hosts one `WorkspacePage`
|
||||
(UserControl) bound to a `WorkspaceViewModel`.
|
||||
- `WorkspaceViewModel` = today's `MainViewModel` generalized: constructed from
|
||||
an `Account` (persistent) or the ephemeral Quick Sync account (today's
|
||||
behavior, nothing saved). One cached VM per account per session so switching
|
||||
accounts never loses a running sync; nav badge reflects each VM's state
|
||||
(running/ok/failed).
|
||||
- Menu bar survives (File/View/Help); Settings keeps global defaults whichseed
|
||||
new accounts.
|
||||
|
||||
Tests: WorkspaceViewModel construction from account values; nav-badge state
|
||||
mapping (pure VM). Tag `beta.7`.
|
||||
|
||||
## Phase 3 — Workspace redesign (the #16-comment direction)
|
||||
|
||||
Inside `WorkspacePage`, replacing the stacked form:
|
||||
|
||||
1. **Connection chip**: org · connected-state · repo count · destination +
|
||||
Edit (opens wizard for accounts; inline flyout for Quick Sync). Org-lookup
|
||||
feedback lives here — the multiplexed StatusText dies.
|
||||
2. **Hero table card**: header + ListView on
|
||||
`LayerFillColorDefaultBrush`, `CornerRadius=8`; attached toolbar =
|
||||
selection summary ("12 of 87 selected"), filter box, options flyout (gear:
|
||||
parallelism, org subfolder), Refresh, and ONE accent CTA "Sync N repos".
|
||||
During a run the toolbar's right side swaps to progress (bar + "34 of 87" +
|
||||
Cancel); progress scoped to the run's selected set (fixes the #22
|
||||
progress-honesty P2).
|
||||
3. **Results InfoBar** (severity-aware): summary + inline Retry failed / Open
|
||||
folder; replaces permanent buttons and the stale Retry visibility bug.
|
||||
4. **Designed empty states**: no token → connect card (+ "how to create a
|
||||
token" link, fixing that P2); connected → "Load N repositories from {org}";
|
||||
filter-no-match state.
|
||||
5. **Reconciled #22 items in this phase**: input disabling while running
|
||||
(CanEditInputs), min window size via `OverlappedPresenter`, sort-header
|
||||
accessibility, table row composed automation names, access keys +
|
||||
accelerators (Ctrl+R / Ctrl+Enter / Esc), microcopy fixes ("Parallel
|
||||
clones", placement preview wording, ellipsis, pluralization).
|
||||
|
||||
Tests: VM-level (selection summary, run-scoped progress, InfoBar state,
|
||||
CanEditInputs); XAML verified by build + launch. Tag `beta.8`.
|
||||
|
||||
## Phase 4 — Account wizard + Sync All
|
||||
|
||||
- `AccountWizardDialog`: 1) name + description → 2) token (stored to vault,
|
||||
validated live via `IOrganizationLister`) → 3) org picker (dropdown from the
|
||||
token; manual entry fallback) → 4) destination + options (seeded from
|
||||
Settings defaults) → summary/save. Same dialog pre-filled = Edit. Delete
|
||||
confirms and removes the vault entry.
|
||||
- **Sync All** (nav pane footer button): sequentially runs each account's
|
||||
WorkspaceViewModel sync; per-account rolled-up progress via nav badges;
|
||||
cancel stops the queue after the in-flight account.
|
||||
- Activity log gains account context in messages (never tokens).
|
||||
|
||||
Tests: wizard VM validation per step; Sync All ordering/cancellation with fake
|
||||
clients. Tag `beta.9`.
|
||||
|
||||
## Phase 5 — Close-out & release
|
||||
|
||||
- Reconcile remaining #22 P3s; README rewrite for accounts + new screenshots
|
||||
(issue #10 — needs owner or an interactive session for captures).
|
||||
- Docs: README, CLI.md, SECURITY.md (vault storage note), RELEASING.md
|
||||
untouched.
|
||||
- Owner gate: confirm self-update beta-chain works installed (#6), then tag
|
||||
**v0.1.0** stable; winget first submission (#7) follows.
|
||||
|
||||
---
|
||||
|
||||
## Risks & watch items
|
||||
|
||||
- **PasswordVault in unpackaged builds**: works without package identity, but
|
||||
verify early in Phase 1 on the unpackaged launch path (it is the dev loop).
|
||||
Fallback if it misbehaves: `CredRead/CredWrite` P/Invoke behind the same
|
||||
`ITokenVault`.
|
||||
- **One VM per account with concurrent runs**: memory/dispatcher pressure is
|
||||
fine at ~5 accounts, but Sync All is deliberately sequential — do not let a
|
||||
future "parallel accounts" ask sneak in without revisiting disk/API limits.
|
||||
- **NavigationView + MenuBar chrome**: two top-level chrome systems can fight;
|
||||
prototype the shell (Phase 2) before styling it (Phase 3).
|
||||
- **Quick Sync regression risk**: it must behave exactly like today's app;
|
||||
its VM tests are the regression net.
|
||||
|
||||
## Execution notes
|
||||
|
||||
- Order is strict: 0 → 1 → 2 → 3 → 4 → 5; each phase is a separate PR through
|
||||
the required checks; no phase starts until the previous merged.
|
||||
- Multi-agent builds continue to use pinned cross-agent contracts (the pattern
|
||||
that has held since the productionize wave); single-writer per file area.
|
||||
- Any change to the four standing defaults happens as a comment on #16 before
|
||||
the affected phase starts.
|
||||
@@ -176,6 +176,42 @@ public sealed class MainViewModelTests
|
||||
Directory.Delete(vm.TargetFolder, recursive: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sync_RepoFailure_RaisesAssistiveAnnouncement()
|
||||
{
|
||||
_git.CloneHandler = (url, path, token, onProgress, ct) =>
|
||||
Path.GetFileName(path) == "bravo"
|
||||
? Task.FromException(new InvalidOperationException("boom"))
|
||||
: Task.CompletedTask;
|
||||
|
||||
var vm = await CreateLoadedViewModelAsync(Repo("alpha"), Repo("bravo"));
|
||||
var announcements = new List<string>();
|
||||
vm.AnnouncementRequested += announcements.Add;
|
||||
|
||||
await vm.SyncCommand.ExecuteAsync(null);
|
||||
|
||||
var announcement = Assert.Single(announcements);
|
||||
Assert.Contains("bravo", announcement);
|
||||
Assert.Contains("failed", announcement);
|
||||
Assert.Contains("boom", announcement);
|
||||
|
||||
Directory.Delete(vm.TargetFolder, recursive: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sync_AllSucceed_RaisesNoAnnouncements()
|
||||
{
|
||||
var vm = await CreateLoadedViewModelAsync(Repo("alpha"), Repo("bravo"));
|
||||
var announcements = new List<string>();
|
||||
vm.AnnouncementRequested += announcements.Add;
|
||||
|
||||
await vm.SyncCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Empty(announcements);
|
||||
|
||||
Directory.Delete(vm.TargetFolder, recursive: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sync_CancelCommand_CancelsRun_AndReportsCanceled()
|
||||
{
|
||||
|
||||
@@ -561,6 +561,13 @@ public sealed partial class MainViewModel : ObservableObject, IDisposable
|
||||
|
||||
// ---------------------------------------------------------------- progress
|
||||
|
||||
/// <summary>
|
||||
/// Raised with a message that assistive technology should announce. Per-repo
|
||||
/// failures use this channel; run summaries land in <see cref="StatusText"/>,
|
||||
/// whose live region already announces changes. Raised on the UI thread.
|
||||
/// </summary>
|
||||
public event Action<string>? AnnouncementRequested;
|
||||
|
||||
/// <summary>Applies one engine progress report to the table. Runs on the UI thread.</summary>
|
||||
private void HandleProgress(RepoProgress report)
|
||||
{
|
||||
@@ -580,6 +587,9 @@ public sealed partial class MainViewModel : ObservableObject, IDisposable
|
||||
if (report.Status == SyncStatus.Failed)
|
||||
{
|
||||
_log.Error($"{report.RepoName} failed: {report.Error}");
|
||||
// Failures never reach StatusText (which has a live region), so assistive
|
||||
// technology hears them only through this explicit channel.
|
||||
AnnouncementRequested?.Invoke($"{report.RepoName} failed. {report.Error}");
|
||||
}
|
||||
|
||||
if (report.Status is SyncStatus.Done or SyncStatus.Failed or SyncStatus.Canceled)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using gclo.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace gclo
|
||||
@@ -16,6 +17,20 @@ namespace gclo
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// App-level so Application.Current.Resources theme lookups (the status
|
||||
// brushes) agree with the visible theme. Only settable here, before any
|
||||
// window exists; mid-session theme changes still use the root element's
|
||||
// RequestedTheme, and status brushes adopt them as row statuses update.
|
||||
string theme = AppSettings.Load().Theme;
|
||||
if (theme == "Light")
|
||||
{
|
||||
RequestedTheme = ApplicationTheme.Light;
|
||||
}
|
||||
else if (theme == "Dark")
|
||||
{
|
||||
RequestedTheme = ApplicationTheme.Dark;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+19
-4
@@ -78,6 +78,7 @@
|
||||
Height="20"
|
||||
Margin="0,0,0,8"
|
||||
VerticalAlignment="Bottom"
|
||||
AutomationProperties.Name="Loading organizations"
|
||||
IsActive="{x:Bind ViewModel.IsLoadingOrgs, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
@@ -134,6 +135,7 @@
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Loading repositories"
|
||||
IsActive="{x:Bind ViewModel.IsLoadingRepos, Mode=OneWay}" />
|
||||
<Button
|
||||
MinWidth="110"
|
||||
@@ -163,9 +165,12 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<ProgressBar
|
||||
AutomationProperties.Name="Overall sync progress"
|
||||
Maximum="{x:Bind ViewModel.TotalCount, Mode=OneWay}"
|
||||
Value="{x:Bind ViewModel.CompletedCount, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
x:Name="StatusTextBlock"
|
||||
AutomationProperties.LiveSetting="Polite"
|
||||
Text="{x:Bind ViewModel.StatusText, Mode=OneWay}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
@@ -191,6 +196,7 @@
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
ToolTipService.ToolTip="Select all"
|
||||
AutomationProperties.Name="Select all repositories"
|
||||
IsChecked="{x:Bind ViewModel.AllSelected, Mode=TwoWay}" />
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
@@ -275,6 +281,7 @@
|
||||
MinWidth="0"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Bind Name, Mode=OneTime}"
|
||||
IsChecked="{x:Bind IsSelected, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
@@ -289,9 +296,16 @@
|
||||
Margin="0,0,12,0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
Text="{x:Bind StatusText, Mode=OneWay}"
|
||||
Foreground="{x:Bind local:StatusFormat.BrushFor(Status), Mode=OneWay}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
Glyph="{x:Bind local:StatusFormat.GlyphFor(Status), Mode=OneWay}"
|
||||
Foreground="{x:Bind local:StatusFormat.BrushFor(Status), Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Text="{x:Bind StatusText, Mode=OneWay}"
|
||||
Foreground="{x:Bind local:StatusFormat.BrushFor(Status), Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<ProgressBar
|
||||
Maximum="1"
|
||||
Value="{x:Bind ProgressValue, Mode=OneWay}"
|
||||
@@ -318,7 +332,7 @@
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="Crimson"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Text="{x:Bind Error, Mode=OneWay}"
|
||||
Visibility="{x:Bind local:StatusFormat.VisibleIf(HasError), Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
@@ -327,6 +341,7 @@
|
||||
Grid.ColumnSpan="5"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Left"
|
||||
AutomationProperties.Name="{x:Bind local:StatusFormat.ResolveAutomationName(Name), Mode=OneTime}"
|
||||
Content="Resolve…"
|
||||
Command="{Binding DataContext.ResolvePathsCommand, ElementName=RepoListView}"
|
||||
CommandParameter="{x:Bind}"
|
||||
|
||||
@@ -31,6 +31,18 @@ namespace gclo
|
||||
ViewModel = new MainViewModel(log: _log);
|
||||
InitializeComponent();
|
||||
|
||||
ViewModel.AnnouncementRequested += AnnounceToAssistiveTechnology;
|
||||
// LiveSetting alone does not announce: XAML never raises LiveRegionChanged
|
||||
// automatically, so each StatusText update must raise it in code-behind.
|
||||
ViewModel.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.StatusText))
|
||||
{
|
||||
// Enqueued so the binding has pushed the new text before the event.
|
||||
DispatcherQueue.TryEnqueue(RaiseStatusLiveRegionChanged);
|
||||
}
|
||||
};
|
||||
|
||||
Title = "gclo — Git Clone Large Organizations";
|
||||
|
||||
// The view model stays UI-free: when ResolvePathsCommand needs the user's
|
||||
@@ -117,6 +129,35 @@ namespace gclo
|
||||
|
||||
private void ExitMenuItem_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void RaiseStatusLiveRegionChanged()
|
||||
{
|
||||
var peer = Microsoft.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer.FromElement(StatusTextBlock)
|
||||
?? Microsoft.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer.CreatePeerForElement(StatusTextBlock);
|
||||
peer?.RaiseAutomationEvent(Microsoft.UI.Xaml.Automation.Peers.AutomationEvents.LiveRegionChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a UIA notification so screen readers hear per-repo failures. Routed
|
||||
/// through the ListView's peer: panels like Grid have no automation peer.
|
||||
/// ImportantMostRecent coalesces bursts when many repositories fail at once.
|
||||
/// </summary>
|
||||
private void AnnounceToAssistiveTechnology(string message)
|
||||
{
|
||||
if (!DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() => AnnounceToAssistiveTechnology(message));
|
||||
return;
|
||||
}
|
||||
|
||||
var peer = Microsoft.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer.FromElement(RepoListView)
|
||||
?? Microsoft.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer.CreatePeerForElement(RepoListView);
|
||||
peer?.RaiseNotificationEvent(
|
||||
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationKind.ActionCompleted,
|
||||
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationProcessing.ImportantMostRecent,
|
||||
message,
|
||||
"gclo-repo-status");
|
||||
}
|
||||
|
||||
private async void ActivityLogMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new LogViewerDialog(_log) { XamlRoot = Content.XamlRoot };
|
||||
|
||||
+37
-13
@@ -8,22 +8,46 @@ namespace gclo;
|
||||
/// <summary>Static helpers for x:Bind function bindings in MainWindow.xaml.</summary>
|
||||
public static class StatusFormat
|
||||
{
|
||||
// Created lazily on first x:Bind evaluation, which happens on the UI thread.
|
||||
private static readonly SolidColorBrush QueuedBrush = new(Colors.Gray);
|
||||
private static readonly SolidColorBrush ActiveBrush = new(Colors.DodgerBlue);
|
||||
private static readonly SolidColorBrush DoneBrush = new(Colors.ForestGreen);
|
||||
private static readonly SolidColorBrush FailedBrush = new(Colors.Crimson);
|
||||
private static readonly SolidColorBrush CanceledBrush = new(Colors.Orange);
|
||||
|
||||
public static Brush BrushFor(SyncStatus status) => status switch
|
||||
/// <summary>
|
||||
/// Theme- and high-contrast-aware brush for a status. Resolved from the app's
|
||||
/// resources on every call so the current theme's instance is returned; x:Bind
|
||||
/// re-invokes this whenever the bound Status changes.
|
||||
/// </summary>
|
||||
public static Brush BrushFor(SyncStatus status) => ResourceBrush(status switch
|
||||
{
|
||||
SyncStatus.Cloning or SyncStatus.Pulling => ActiveBrush,
|
||||
SyncStatus.Done => DoneBrush,
|
||||
SyncStatus.Failed => FailedBrush,
|
||||
SyncStatus.Canceled => CanceledBrush,
|
||||
_ => QueuedBrush,
|
||||
SyncStatus.Cloning or SyncStatus.Pulling => "SystemFillColorAttentionBrush",
|
||||
SyncStatus.Done => "SystemFillColorSuccessBrush",
|
||||
SyncStatus.Failed => "SystemFillColorCriticalBrush",
|
||||
SyncStatus.Canceled => "SystemFillColorCautionBrush",
|
||||
_ => "TextFillColorSecondaryBrush",
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Segoe Fluent Icons glyph for a status, shown beside the status text so state
|
||||
/// is never communicated by color alone.
|
||||
/// </summary>
|
||||
public static string GlyphFor(SyncStatus status) => status switch
|
||||
{
|
||||
SyncStatus.Queued => "", // Recent (clock)
|
||||
SyncStatus.Cloning => "", // Download
|
||||
SyncStatus.Pulling => "", // Sync
|
||||
SyncStatus.Done => "", // CheckMark
|
||||
SyncStatus.Failed => "", // Error
|
||||
SyncStatus.Canceled => "", // Cancel
|
||||
_ => "",
|
||||
};
|
||||
|
||||
/// <summary>Screen-reader name for a row's path-recovery link.</summary>
|
||||
public static string ResolveAutomationName(string repoName)
|
||||
=> $"Resolve invalid paths for {repoName}";
|
||||
|
||||
private static Brush ResourceBrush(string key)
|
||||
=> Application.Current.Resources.TryGetValue(key, out object? value) && value is Brush brush
|
||||
? brush
|
||||
// Unreachable with XamlControlsResources merged; a visible-but-neutral
|
||||
// fallback beats crashing the row template if a key ever disappears.
|
||||
: new SolidColorBrush(Colors.Gray);
|
||||
|
||||
public static Visibility VisibleIf(bool value)
|
||||
=> value ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
|
||||
Reference in new issue
Block a user