Skip to content

Commit c82b737

Browse files
mohnjilesclaude
andcommitted
Add in-app documentation viewer page
Adds a Documentation footer page that fetches the docs/ folder from the repository, renders markdown with the existing Markdown.Avalonia control, and handles internal navigation, external links, and relative images. - Fetch docs tree via new Refit IGitHubContentApi (GitHub git/trees) - DocumentationService groups pages into humanized sections (README first), caches the tree listing and page markdown to disk (1 day TTL) and serves from cache on network failure - DocumentationPathResolver: pure helpers for humanizing names and resolving relative .md links / image URLs (unit tested) - DocumentationMarkdownViewer routes hyperlink clicks through a command (relative .md navigates in-app, http(s) opens in the browser) and rewrites relative image paths to raw URLs - Loading, offline/error (with Retry), and "docs not available yet" states Owner/repo/branch live as consts in DocumentationConstants (branch "main"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d97f6cc commit c82b737

17 files changed

Lines changed: 1414 additions & 1 deletion

StabilityMatrix.Avalonia/App.axaml.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,11 @@ internal static void ConfigurePageViewModels(IServiceCollection services)
464464
provider.GetRequiredService<OutputsPageViewModel>(),
465465
provider.GetRequiredService<WorkflowsPageViewModel>(),
466466
},
467-
FooterPages = { provider.GetRequiredService<SettingsViewModel>() },
467+
FooterPages =
468+
{
469+
provider.GetRequiredService<DocumentationViewModel>(),
470+
provider.GetRequiredService<SettingsViewModel>(),
471+
},
468472
});
469473
}
470474

@@ -838,6 +842,15 @@ internal static IServiceCollection ConfigureServices(bool disableMessagePipeInte
838842
})
839843
.AddPolicyHandler(retryPolicy);
840844

845+
services
846+
.AddRefitClient<IGitHubContentApi>(defaultRefitSettings)
847+
.ConfigureHttpClient(c =>
848+
{
849+
c.BaseAddress = new Uri("https://api.github.com");
850+
c.Timeout = TimeSpan.FromMinutes(1);
851+
})
852+
.AddPolicyHandler(retryPolicy);
853+
841854
services
842855
.AddRefitClient<IHuggingFaceApi>(defaultRefitSettings) // Assuming defaultRefitSettings is suitable
843856
.ConfigureHttpClient(c =>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Windows.Input;
3+
using Avalonia;
4+
using Markdown.Avalonia;
5+
6+
namespace StabilityMatrix.Avalonia.Controls;
7+
8+
/// <summary>
9+
/// A <see cref="BetterMarkdownScrollViewer"/> that routes hyperlink clicks through a
10+
/// bindable <see cref="LinkCommand"/> (so relative <c>.md</c> links can navigate in-app
11+
/// and external links can open in the browser) and resolves relative image paths against
12+
/// <see cref="ImageBaseUrl"/> via the engine's asset path root.
13+
/// </summary>
14+
public class DocumentationMarkdownViewer : BetterMarkdownScrollViewer
15+
{
16+
/// <summary>
17+
/// Command invoked when a hyperlink is clicked. The command parameter is the raw href string.
18+
/// </summary>
19+
public static readonly StyledProperty<ICommand?> LinkCommandProperty = AvaloniaProperty.Register<
20+
DocumentationMarkdownViewer,
21+
ICommand?
22+
>(nameof(LinkCommand));
23+
24+
/// <summary>
25+
/// Base URL used to resolve relative image paths in the rendered markdown
26+
/// (e.g. the raw URL of the current page's folder).
27+
/// </summary>
28+
public static readonly StyledProperty<string?> ImageBaseUrlProperty = AvaloniaProperty.Register<
29+
DocumentationMarkdownViewer,
30+
string?
31+
>(nameof(ImageBaseUrl));
32+
33+
public ICommand? LinkCommand
34+
{
35+
get => GetValue(LinkCommandProperty);
36+
set => SetValue(LinkCommandProperty, value);
37+
}
38+
39+
public string? ImageBaseUrl
40+
{
41+
get => GetValue(ImageBaseUrlProperty);
42+
set => SetValue(ImageBaseUrlProperty, value);
43+
}
44+
45+
public DocumentationMarkdownViewer()
46+
{
47+
ApplyLinkCommand();
48+
ApplyImageBaseUrl();
49+
}
50+
51+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
52+
{
53+
base.OnPropertyChanged(change);
54+
55+
if (change.Property == LinkCommandProperty)
56+
{
57+
ApplyLinkCommand();
58+
}
59+
else if (change.Property == ImageBaseUrlProperty)
60+
{
61+
ApplyImageBaseUrl();
62+
}
63+
}
64+
65+
private void ApplyLinkCommand()
66+
{
67+
// The engine (IMarkdownEngine) owns the HyperlinkCommand used for all rendered links.
68+
if (Engine is IMarkdownEngine engine)
69+
{
70+
engine.HyperlinkCommand = LinkCommand;
71+
}
72+
}
73+
74+
private void ApplyImageBaseUrl()
75+
{
76+
// AssetPathRoot flows through to the engine's bitmap loader so relative image
77+
// paths resolve against the raw docs URL.
78+
AssetPathRoot = ImageBaseUrl ?? string.Empty;
79+
}
80+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using CommunityToolkit.Mvvm.ComponentModel;
2+
3+
namespace StabilityMatrix.Avalonia.ViewModels.Documentation;
4+
5+
/// <summary>
6+
/// A single navigable documentation page entry in the sidebar.
7+
/// </summary>
8+
public partial class DocumentationPageNavItem : ObservableObject
9+
{
10+
/// <summary>Display title, e.g. "Overview".</summary>
11+
public required string Title { get; init; }
12+
13+
/// <summary>Path relative to the docs root, e.g. <c>getting-started/overview.md</c>.</summary>
14+
public required string Path { get; init; }
15+
16+
[ObservableProperty]
17+
private bool isSelected;
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Collections.Generic;
2+
3+
namespace StabilityMatrix.Avalonia.ViewModels.Documentation;
4+
5+
/// <summary>
6+
/// A section grouping in the documentation sidebar (e.g. "Getting Started").
7+
/// </summary>
8+
public class DocumentationSectionNavItem
9+
{
10+
/// <summary>Section title. Empty for the root section (renders without a header).</summary>
11+
public required string Title { get; init; }
12+
13+
/// <summary>Whether this section has a visible header (i.e. is not the root section).</summary>
14+
public bool HasHeader => !string.IsNullOrEmpty(Title);
15+
16+
/// <summary>Pages within this section.</summary>
17+
public required IReadOnlyList<DocumentationPageNavItem> Pages { get; init; }
18+
}

0 commit comments

Comments
 (0)