Skip to content

Commit 73234cd

Browse files
mohnjilesclaude
andcommitted
feat: contextual docs help buttons, F1, and settings entry point
Makes the in-app documentation reachable from where users get stuck instead of from a sidebar row they rarely need. - DocsHelpButton: one reusable ? control that resolves navigation itself, so adding help to a surface is a single line of XAML with no view model plumbing - DocumentationPages: the docs paths help buttons link to, in one place, so a moved page is renamed once and a typo fails to compile - DocumentationViewModel.RequestPage queues a deep link until the nav tree loads, so a ? click can't be overwritten by the default landing page - Drops the sidebar footer item; the global entry points are now F1 and a Documentation button in Settings > About - ? buttons on: Settings header, Environment Variables, App Folders, Package Manager header, package install details, PyTorch index, Inference header, console - Localizes the viewer's user-facing strings via Resources Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3eb75ab commit 73234cd

22 files changed

Lines changed: 503 additions & 98 deletions

StabilityMatrix.Avalonia/App.axaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ internal static void ConfigurePageViewModels(IServiceCollection services)
451451
provider.GetRequiredService<ISecretsManager>(),
452452
provider.GetRequiredService<INavigationService<MainWindowViewModel>>(),
453453
provider.GetRequiredService<INavigationService<SettingsViewModel>>(),
454+
provider.GetRequiredService<IDocumentationNavigationService>(),
454455
provider.GetRequiredService<IDistributedSubscriber<string, Uri>>()
455456
)
456457
{
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using Avalonia;
3+
using Avalonia.Controls;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using NLog;
6+
using StabilityMatrix.Avalonia.Services;
7+
8+
namespace StabilityMatrix.Avalonia.Controls;
9+
10+
/// <summary>
11+
/// Contextual help button that opens the in-app documentation viewer at <see cref="DocsPath"/>.
12+
/// Resolves navigation itself so that adding help to a surface needs no view model plumbing.
13+
/// </summary>
14+
/// <remarks>
15+
/// Prefer a <see cref="Core.Models.Documentation.DocumentationPages"/> constant over a literal
16+
/// path so a moved page is fixed in one place.
17+
/// </remarks>
18+
public class DocsHelpButton : Button
19+
{
20+
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
21+
22+
public static readonly StyledProperty<string?> DocsPathProperty = AvaloniaProperty.Register<
23+
DocsHelpButton,
24+
string?
25+
>(nameof(DocsPath));
26+
27+
/// <summary>
28+
/// Page path relative to the docs root, e.g. <c>advanced/environment-variables.md</c>.
29+
/// </summary>
30+
public string? DocsPath
31+
{
32+
get => GetValue(DocsPathProperty);
33+
set => SetValue(DocsPathProperty, value);
34+
}
35+
36+
public static readonly StyledProperty<string?> AnchorProperty = AvaloniaProperty.Register<
37+
DocsHelpButton,
38+
string?
39+
>(nameof(Anchor));
40+
41+
/// <summary>
42+
/// Optional heading slug within the page to scroll to, e.g. <c>setting-a-variable</c>.
43+
/// </summary>
44+
public string? Anchor
45+
{
46+
get => GetValue(AnchorProperty);
47+
set => SetValue(AnchorProperty, value);
48+
}
49+
50+
protected override Type StyleKeyOverride => typeof(DocsHelpButton);
51+
52+
protected override void OnClick()
53+
{
54+
base.OnClick();
55+
56+
if (Design.IsDesignMode)
57+
return;
58+
59+
if (string.IsNullOrWhiteSpace(DocsPath))
60+
{
61+
Logger.Warn("{Control} clicked with no {Property} set", nameof(DocsHelpButton), nameof(DocsPath));
62+
return;
63+
}
64+
65+
App.Services?.GetService<IDocumentationNavigationService>()?.OpenDocumentation(DocsPath, Anchor);
66+
}
67+
}

StabilityMatrix.Avalonia/Languages/Resources.Designer.cs

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

StabilityMatrix.Avalonia/Languages/Resources.resx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,4 +1832,31 @@ Account tokens will not be viewable after saving, please make a note of them if
18321832
<data name="Action_CustomFolderEllipsis" xml:space="preserve">
18331833
<value>Custom...</value>
18341834
</data>
1835+
<data name="Label_Documentation" xml:space="preserve">
1836+
<value>Documentation</value>
1837+
</data>
1838+
<data name="Label_ViewDocumentation" xml:space="preserve">
1839+
<value>View documentation</value>
1840+
</data>
1841+
<data name="Label_DocumentationNotAvailable" xml:space="preserve">
1842+
<value>Documentation is not available yet</value>
1843+
</data>
1844+
<data name="Label_ZoomIn" xml:space="preserve">
1845+
<value>Zoom in</value>
1846+
</data>
1847+
<data name="Label_ZoomOut" xml:space="preserve">
1848+
<value>Zoom out</value>
1849+
</data>
1850+
<data name="Label_ResetZoom" xml:space="preserve">
1851+
<value>Reset zoom</value>
1852+
</data>
1853+
<data name="Text_DocumentationNotAvailable" xml:space="preserve">
1854+
<value>The documentation is coming soon. Please check back in a future release.</value>
1855+
</data>
1856+
<data name="Error_DocumentationListingLoadFailed" xml:space="preserve">
1857+
<value>Could not load the documentation listing. Check your connection and try again.</value>
1858+
</data>
1859+
<data name="Error_DocumentationPageLoadFailed" xml:space="preserve">
1860+
<value>Could not load this page. Check your connection and try again.</value>
1861+
</data>
18351862
</root>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using Injectio.Attributes;
3+
using StabilityMatrix.Avalonia.Animations;
4+
using StabilityMatrix.Avalonia.ViewModels;
5+
6+
namespace StabilityMatrix.Avalonia.Services;
7+
8+
/// <summary>
9+
/// Single entry point for every contextual help affordance in the app, so that call sites
10+
/// only name the docs page they want and never touch the navigation frame or the viewer's state.
11+
/// </summary>
12+
[RegisterSingleton<IDocumentationNavigationService, DocumentationNavigationService>]
13+
public class DocumentationNavigationService(
14+
INavigationService<MainWindowViewModel> navigationService,
15+
Lazy<DocumentationViewModel> documentationViewModel
16+
) : IDocumentationNavigationService
17+
{
18+
/// <inheritdoc />
19+
public void OpenDocumentation(string? docsRelativePath = null, string? anchor = null)
20+
{
21+
// Queue the target before navigating: on the viewer's first load the nav tree would
22+
// otherwise select its landing page after we navigate, discarding the request.
23+
if (!string.IsNullOrWhiteSpace(docsRelativePath))
24+
{
25+
documentationViewModel.Value.RequestPage(docsRelativePath, anchor);
26+
}
27+
28+
navigationService.NavigateTo<DocumentationViewModel>(new BetterEntranceNavigationTransition());
29+
}
30+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace StabilityMatrix.Avalonia.Services;
2+
3+
/// <summary>
4+
/// Opens the in-app documentation viewer, optionally at a specific page.
5+
/// </summary>
6+
public interface IDocumentationNavigationService
7+
{
8+
/// <summary>
9+
/// Navigates to the documentation viewer.
10+
/// </summary>
11+
/// <param name="docsRelativePath">
12+
/// Page path relative to the docs root, e.g. <c>advanced/environment-variables.md</c>.
13+
/// When null, the viewer opens on its landing page.
14+
/// </param>
15+
/// <param name="anchor">Optional heading slug to scroll to within the page.</param>
16+
void OpenDocumentation(string? docsRelativePath = null, string? anchor = null);
17+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<ResourceDictionary
2+
xmlns="https://github.com/avaloniaui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
5+
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
6+
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages">
7+
8+
<Design.PreviewWith>
9+
<StackPanel
10+
Margin="8"
11+
Orientation="Horizontal"
12+
Spacing="6">
13+
<controls:DocsHelpButton DocsPath="README.md" />
14+
<controls:DocsHelpButton DocsPath="README.md" IsEnabled="False" />
15+
</StackPanel>
16+
</Design.PreviewWith>
17+
18+
<ControlTheme x:Key="{x:Type controls:DocsHelpButton}" TargetType="controls:DocsHelpButton">
19+
20+
<Setter Property="Background" Value="Transparent" />
21+
<Setter Property="BorderThickness" Value="0" />
22+
<Setter Property="Padding" Value="4" />
23+
<Setter Property="CornerRadius" Value="4" />
24+
<Setter Property="Cursor" Value="Hand" />
25+
<Setter Property="VerticalAlignment" Value="Center" />
26+
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
27+
<Setter Property="ToolTip.Tip" Value="{x:Static lang:Resources.Label_ViewDocumentation}" />
28+
29+
<Setter Property="Template">
30+
<ControlTemplate>
31+
<Border
32+
Name="Root"
33+
Padding="{TemplateBinding Padding}"
34+
Background="{TemplateBinding Background}"
35+
CornerRadius="{TemplateBinding CornerRadius}">
36+
<!-- FluentIcons SymbolIcon measures greedily without explicit Width/Height -->
37+
<fluentIcons:SymbolIcon
38+
Width="16"
39+
Height="16"
40+
FontSize="16"
41+
Foreground="{TemplateBinding Foreground}"
42+
Symbol="QuestionCircle" />
43+
</Border>
44+
</ControlTemplate>
45+
</Setter>
46+
47+
<Style Selector="^:pointerover">
48+
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
49+
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
50+
</Style>
51+
<Style Selector="^:pressed">
52+
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
53+
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
54+
</Style>
55+
<Style Selector="^:disabled">
56+
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
57+
<Setter Property="Background" Value="Transparent" />
58+
</Style>
59+
60+
</ControlTheme>
61+
62+
</ResourceDictionary>

StabilityMatrix.Avalonia/Styles/ControlThemes/_index.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
x:CompileBindings="True">
55
<ResourceDictionary.MergedDictionaries>
66
<ResourceInclude Source="avares://StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml" />
7+
<ResourceInclude Source="avares://StabilityMatrix.Avalonia/Styles/ControlThemes/DocsHelpButtonStyles.axaml" />
78
<ResourceInclude Source="avares://StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml" />
89
<ResourceInclude Source="avares://StabilityMatrix.Avalonia/Styles/ControlThemes/LabelStyles.axaml" />
910
<ResourceInclude Source="avares://StabilityMatrix.Avalonia/Styles/ControlThemes/LabelStyles.Dark.axaml" />

StabilityMatrix.Avalonia/ViewModels/DocumentationViewModel.cs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using FluentIcons.Common;
1313
using Injectio.Attributes;
1414
using Microsoft.Extensions.Logging;
15+
using StabilityMatrix.Avalonia.Languages;
1516
using StabilityMatrix.Avalonia.ViewModels.Base;
1617
using StabilityMatrix.Avalonia.ViewModels.Documentation;
1718
using StabilityMatrix.Core.Attributes;
@@ -35,7 +36,7 @@ public partial class DocumentationViewModel : PageViewModelBase
3536
private readonly IDocumentationService documentationService;
3637
private readonly ISettingsManager settingsManager;
3738

38-
public override string Title => "Documentation";
39+
public override string Title => Resources.Label_Documentation;
3940

4041
public override IconSource IconSource =>
4142
new SymbolIconSource { Symbol = Symbol.BookOpen, IconVariant = IconVariant.Filled };
@@ -105,6 +106,12 @@ public partial class DocumentationViewModel : PageViewModelBase
105106
/// <summary>Anchor slug to scroll to after the next page load completes (cross-page anchor links).</summary>
106107
private string? pendingAnchor;
107108

109+
/// <summary>
110+
/// Page requested via <see cref="RequestPage"/> before the nav tree was available.
111+
/// <see cref="LoadTreeAsync"/> applies it in place of the default landing page.
112+
/// </summary>
113+
private (string Path, string? Anchor)? pendingDeepLink;
114+
108115
public DocumentationViewModel(
109116
ILogger<DocumentationViewModel> logger,
110117
IDocumentationService documentationService,
@@ -141,6 +148,31 @@ public override async Task OnLoadedAsync()
141148
}
142149
}
143150

151+
/// <summary>
152+
/// Opens a specific documentation page, for contextual help entry points such as a
153+
/// <c>?</c> button. Safe to call before this page has ever been shown: the request is
154+
/// queued and applied once the nav tree loads, so it cannot be overwritten by the
155+
/// default landing page.
156+
/// </summary>
157+
/// <param name="docsRelativePath">
158+
/// Path relative to the docs root, e.g. <c>advanced/environment-variables.md</c>.
159+
/// </param>
160+
/// <param name="anchor">Optional heading slug to scroll to.</param>
161+
public void RequestPage(string docsRelativePath, string? anchor = null)
162+
{
163+
if (string.IsNullOrWhiteSpace(docsRelativePath))
164+
return;
165+
166+
if (hasLoadedTree)
167+
{
168+
NavigateToPath(docsRelativePath, anchor);
169+
}
170+
else
171+
{
172+
pendingDeepLink = (docsRelativePath, anchor);
173+
}
174+
}
175+
144176
[RelayCommand]
145177
private async Task RetryAsync()
146178
{
@@ -203,11 +235,20 @@ private async Task LoadTreeAsync(bool forceRefresh = false)
203235

204236
hasLoadedTree = true;
205237

206-
// Select the first available page (docs README landing page comes first).
207-
var firstPage = Sections.SelectMany(s => s.Pages).FirstOrDefault();
208-
if (firstPage is not null)
238+
// A queued deep link (contextual help button) wins over the default landing page.
239+
if (pendingDeepLink is { } deepLink)
209240
{
210-
SelectedPage = firstPage;
241+
pendingDeepLink = null;
242+
NavigateToPath(deepLink.Path, deepLink.Anchor);
243+
}
244+
else
245+
{
246+
// Select the first available page (docs README landing page comes first).
247+
var firstPage = Sections.SelectMany(s => s.Pages).FirstOrDefault();
248+
if (firstPage is not null)
249+
{
250+
SelectedPage = firstPage;
251+
}
211252
}
212253
}
213254
catch (DocumentationNotAvailableException e)
@@ -220,7 +261,7 @@ private async Task LoadTreeAsync(bool forceRefresh = false)
220261
catch (Exception e)
221262
{
222263
logger.LogWarning(e, "Failed to load documentation tree");
223-
ErrorMessage = "Could not load the documentation listing. Check your connection and try again.";
264+
ErrorMessage = Resources.Error_DocumentationListingLoadFailed;
224265
Sections.Clear();
225266
TreeItems.Clear();
226267
}
@@ -296,7 +337,7 @@ private async Task LoadPageAsync(string docsRelativePath, string? anchor = null)
296337
catch (Exception e)
297338
{
298339
logger.LogWarning(e, "Failed to load documentation page {Path}", docsRelativePath);
299-
ErrorMessage = "Could not load this page. Check your connection and try again.";
340+
ErrorMessage = Resources.Error_DocumentationPageLoadFailed;
300341
CurrentMarkdown = null;
301342
}
302343
finally

0 commit comments

Comments
 (0)