diff --git a/Logo.svg b/Logo.svg index 3a34881b..4386c2a2 100644 --- a/Logo.svg +++ b/Logo.svg @@ -1,22 +1,31 @@ - + Linq2GraphQL - - - + + + + + + - - - - - - - - - - - - - - + + + + + + + + + diff --git a/docs/Linq2GraphQL.Docs/App.razor b/docs/Linq2GraphQL.Docs/App.razor index 54e55066..b1aad030 100644 --- a/docs/Linq2GraphQL.Docs/App.razor +++ b/docs/Linq2GraphQL.Docs/App.razor @@ -4,9 +4,13 @@ - @* Not found *@ + Page not found - Linq2GraphQL -

Sorry, there's nothing at this address.

+

Page not found

+ diff --git a/docs/Linq2GraphQL.Docs/Components/GenerateClient.razor b/docs/Linq2GraphQL.Docs/Components/GenerateClient.razor index d8bb2537..b3bde0df 100644 --- a/docs/Linq2GraphQL.Docs/Components/GenerateClient.razor +++ b/docs/Linq2GraphQL.Docs/Components/GenerateClient.razor @@ -1,8 +1,14 @@ @page "/generate-client" -

Generate Client

+Online generator - Linq2GraphQL -
+

Online generator

+

+ Generate a client in the browser from a GraphQL endpoint or a schema, without installing anything. The + result is the same code the generator writes, downloaded as a zip file. +

+ +
diff --git a/docs/Linq2GraphQL.Docs/Icons/Icons.cs b/docs/Linq2GraphQL.Docs/Icons/Icons.cs index ab75396c..ce27d549 100644 --- a/docs/Linq2GraphQL.Docs/Icons/Icons.cs +++ b/docs/Linq2GraphQL.Docs/Icons/Icons.cs @@ -10,5 +10,25 @@ public static class Icons public static IIconType Terminal_2 => new TablerIcon(@""); public static IIconType Book => new TablerIcon(@""); public static IIconType Copy => new TablerIcon(@""); + public static IIconType Sun => new TablerIcon(@""); + public static IIconType Moon => new TablerIcon(@""); + public static IIconType BrandGithub => new TablerIcon(@""); + public static IIconType Menu => new TablerIcon(@""); + public static IIconType X => new TablerIcon(@""); + public static IIconType ArrowLeft => new TablerIcon(@""); + public static IIconType ArrowRight => new TablerIcon(@""); + public static IIconType Check => new TablerIcon(@""); + public static IIconType Pencil => new TablerIcon(@""); + public static IIconType Package => new TablerIcon(@""); + public static IIconType FileCode => new TablerIcon(@""); + public static IIconType Filter => new TablerIcon(@""); + public static IIconType Broadcast => new TablerIcon(@""); + public static IIconType ChevronsRight => new TablerIcon(@""); + public static IIconType AlertTriangle => new TablerIcon(@""); + public static IIconType Key => new TablerIcon(@""); + public static IIconType Hierarchy => new TablerIcon(@""); + public static IIconType CircleDashed => new TablerIcon(@""); + public static IIconType Robot => new TablerIcon(@""); + public static IIconType ExternalLink => new TablerIcon(@""); } } diff --git a/docs/Linq2GraphQL.Docs/Pages/Index.razor b/docs/Linq2GraphQL.Docs/Pages/Index.razor index 423d5283..85ac602e 100644 --- a/docs/Linq2GraphQL.Docs/Pages/Index.razor +++ b/docs/Linq2GraphQL.Docs/Pages/Index.razor @@ -2,61 +2,74 @@ Linq2GraphQL - typed GraphQL for .NET -
- -

Linq2GraphQL

-

- Query any GraphQL API from C# with LINQ. Generate a strongly-typed client from the schema, write - your queries as lambdas the compiler checks, and get typed objects back. -

-
- Get started - Generate a client online - GitHub +
+
+ Typed GraphQL client for .NET +

Query GraphQL
with LINQ.

+

+ Generate a strongly-typed client from any GraphQL schema, write your queries as lambdas the + compiler checks, and get typed objects back. +

+ +
+ +
+
-
- - Latest stable version on NuGet - - - Latest prerelease version on NuGet - +
+ +
-
-

What is Linq2GraphQL?

-

- GraphQL lets a client ask for exactly the fields it needs. From C# that usually means writing query - strings by hand, keeping them in sync with the schema, and mapping the JSON that comes back onto - classes that slowly drift from what the server actually returns. Mistakes show up at runtime, as - errors from the server or as properties that are silently null. -

-

- Linq2GraphQL removes that gap. A generator reads the schema and writes a C# client for it: a class - for every type, input and enum, and a method for every query, mutation and subscription, with the - arguments as parameters. You describe what to fetch with ordinary lambdas, the library turns them - into GraphQL, sends the request, and deserializes the response into the same generated types. If a - field is renamed or removed, regenerate the client and the compiler tells you everywhere it was used. -

+
+
+

What is Linq2GraphQL?

+
+
+

+ GraphQL lets a client ask for exactly the fields it needs. From C# that usually means writing query + strings by hand, keeping them in sync with the schema, and mapping the JSON that comes back onto + classes that slowly drift from what the server actually returns. Mistakes show up at runtime, as + errors from the server or as properties that are silently null. +

+

+ Linq2GraphQL removes that gap. A generator reads the schema and writes a C# client for it: a class + for every type, input and enum, and a method for every query, mutation and subscription, with the + arguments as parameters. You describe what to fetch with ordinary lambdas, the library turns them + into GraphQL, sends the request, and deserializes the response into the same generated types. If a + field is renamed or removed, regenerate the client and the compiler tells you everywhere it was used. +

+
-
-

A first look

-

- This query asks the public Star Wars API - for its first three films and the characters in each. The C# on the left becomes the GraphQL on the - right; you never write the GraphQL yourself. -

-
-
- -
-
- -
+
+
+

A first look

+

+ This query asks the public Star Wars API + for its first three films and the characters in each. The C# on the left becomes the GraphQL on the + right; you never write the GraphQL yourself. +

+
+
+ +
-

+

films is a List<Film>. The scalar fields of a type, like title and releaseDate, are included automatically. Related objects, like the characters here, and lists of scalars are fetched only when you Include them, so a query @@ -64,140 +77,113 @@ because the field takes arguments of its own. Arguments are always sent as variables, never pasted into the query text.

-
- -
-

Only the fields you ask for

-

- Returning whole objects, as above, is just the default. Project the result in Select and the - query asks for the fields your projection reads and nothing else, however deep they are: -

-
-
- -
-
- -
-
-

- Project into an anonymous type, a class of your own or a single value. +

+ Returning whole objects is just the default. Project the result in Select, as in the query + at the top of this page, and the query asks for the fields your projection reads and nothing else, + however deep they are. Project into an anonymous type, a class of your own or a single value; Selecting fields shows each of them with the GraphQL it sends.

-
-

How it works

-
-
-
-
-
1
-

Generate

-

- Point the Linq2GraphQL generator at a GraphQL endpoint or an SDL - schema file, or use the online generator. You get plain C# - source files to add to your project. -

-
-
+
+
+

How it works

+
+
+
+
1
+

Generate

+

+ Point the Linq2GraphQL generator at a GraphQL endpoint or an SDL + schema file, or use the online generator. You get plain C# + source files to add to your project. +

-
-
-
-
2
-

Query

-

- Pick an operation on Query, Mutation or Subscription, - then shape the result with Include and Select. Arguments are - method parameters and are sent as GraphQL variables. -

-
-
+
+
2
+

Query

+

+ Pick an operation on Query, Mutation or Subscription, + then shape the result with Include and Select. Arguments are + method parameters and are sent as GraphQL variables. +

-
-
-
-
3
-

Execute

-

- ExecuteAsync sends the request through an HttpClient you configure - in dependency injection, and returns typed objects, or a result with the errors if you - would rather not handle exceptions. -

-
-
+
+
3
+

Execute

+

+ ExecuteAsync sends the request through an HttpClient you configure + in dependency injection, and returns typed objects, or a result with the errors if you + would rather not handle exceptions. +

-
-

Features

-
+
+
+

Features

+
+
@foreach (var feature in features) { -
-
-
-

@feature.Title

-

@feature.Text

- @if (feature.Href != null) - { - Read more - } -
+ @if (feature.Href != null) + { + + +

@feature.Title

+

@feature.Text

+ Read more +
+ } + else + { +
+ +

@feature.Title

+

@feature.Text

-
+ } }
-
-

Packages

-

- Linq2GraphQL is split into a generator you run at design time and runtime packages your generated - client depends on. All of them target .NET 10. -

-
- - - - - - - - - @foreach (var package in packages) - { - - - - - } - -
PackageWhat it is for
- @package.Id - @package.Text
+
+
+

Packages

+

+ Linq2GraphQL is split into a generator you run at design time and runtime packages your generated + client depends on. All of them target .NET 10. +

+
+
+ @foreach (var package in packages) + { + + + + @package.Id +

@package.Text

+
+
+ }
-
-

Where to next

- +
+
+
+

Ready to try it?

+

Generate a client, register it and run a first query in four steps.

+
+ Get started +
+

+ Found a bug or missing a feature? Open an issue. + Linq2GraphQL is open source under the MIT license and inspired by + GraphQLinq, thank you + Giorgi. +

@code { @@ -284,31 +270,31 @@ } """; - private record Feature(string Title, string Text, string Href = null); + private record Feature(IIconType Icon, string Title, string Text, string Href = null); private readonly Feature[] features = [ - new("Generated from your schema", + new(Icons.FileCode, "Generated from your schema", "Generate from a live endpoint (with a bearer token if it needs one), an SDL file such as one exported from HotChocolate, or an introspection result. Types, inputs, enums, interfaces and the client itself are generated, and the client and its operations come with interfaces you can mock in tests."), - new("Include and Select", + new(Icons.Filter, "Include and Select", "Fetch whole objects, with scalar fields included automatically and related objects added with Include, or project into an anonymous type or a class of your own and fetch only the fields the projection reads.", "selecting-fields"), - new("Queries, mutations and subscriptions", + new(Icons.Broadcast, "Queries, mutations and subscriptions", "Mutations take generated input classes. Subscriptions are IObservable streams over graphql-ws WebSockets or server-sent events.", "subscriptions"), - new("Cursor paging", + new(Icons.ChevronsRight, "Cursor paging", "AsPager turns a Relay style connection into next and previous page calls, and GetAllPagesAsync streams every page as an IAsyncEnumerable.", "paging"), - new("Errors your way", + new(Icons.AlertTriangle, "Errors your way", "ExecuteAsync throws a GraphQueryExecutionException with the server's errors, classified into codes like Authentication or Validation. ExecuteWithResultAsync returns the data and the errors side by side instead.", "error-handling"), - new("Headers and access tokens", + new(Icons.Key, "Headers and access tokens", "Set headers for every request in the options, or derive a client per user with WithAccessToken for on-behalf-of tokens. Derived clients share the connection and are cheap to create."), - new("Interfaces and unions", + new(Icons.Hierarchy, "Interfaces and unions", "Results are deserialized into the concrete generated type, and the fields of a specific implementation can be included on demand."), - new("Nullable clients", + new(Icons.CircleDashed, "Nullable clients", "Generate a client annotated for nullable reference types, so the compiler knows which fields the schema allows to be null, or a classic one without annotations."), - new("Agent mode (preview)", + new(Icons.Robot, "Agent mode (preview)", "Linq2GraphQL.Client.AI turns a client into tools for Microsoft.Extensions.AI, so an AI agent can query your API in GraphQL, limited to the operations you allow and validated before anything is sent.", "agent-mode") ]; diff --git a/docs/Linq2GraphQL.Docs/Pages/SelectingFields.razor b/docs/Linq2GraphQL.Docs/Pages/SelectingFields.razor index 3ea8c399..e6bb26fa 100644 --- a/docs/Linq2GraphQL.Docs/Pages/SelectingFields.razor +++ b/docs/Linq2GraphQL.Docs/Pages/SelectingFields.razor @@ -83,7 +83,7 @@

Pick a few fields

Project into an anonymous type with the fields you want:

-
+
@@ -101,7 +101,7 @@ CharacterConnection(null, 3) asks for the first three characters of each film.

-
+
@@ -116,7 +116,7 @@

Select a single value

A projection doesn't have to be an object. Select one field and you get a sequence of its values:

-
+
@@ -129,7 +129,7 @@ items in it:

-
+
diff --git a/docs/Linq2GraphQL.Docs/Shared/BrandMark.razor b/docs/Linq2GraphQL.Docs/Shared/BrandMark.razor new file mode 100644 index 00000000..75cd79b4 --- /dev/null +++ b/docs/Linq2GraphQL.Docs/Shared/BrandMark.razor @@ -0,0 +1,24 @@ + + +@code { + [Parameter] public int Size { get; set; } = 32; + + [Parameter] public string Class { get; set; } +} diff --git a/docs/Linq2GraphQL.Docs/Shared/CodeBlock.razor b/docs/Linq2GraphQL.Docs/Shared/CodeBlock.razor index 29cf01eb..512e1dbe 100644 --- a/docs/Linq2GraphQL.Docs/Shared/CodeBlock.razor +++ b/docs/Linq2GraphQL.Docs/Shared/CodeBlock.razor @@ -1,29 +1,43 @@ -@using ColorCode +@using TabBlazor.Services +@inject TablerService TablerService -@* A code sample. C# is syntax highlighted with ColorCode, anything else is shown as plain text. *@ -
- @if (!string.IsNullOrEmpty(Title)) - { -
@Title
- } - @if (Language == "csharp") - { - @((MarkupString)new HtmlClassFormatter().GetHtmlString(Text, Languages.CSharp)) - } - else - { -
@Text
- } -
+
+
+ @SyntaxHighlighter.Label(Language) + @if (!string.IsNullOrEmpty(Title)) + { + @Title + } + +
+
+ @((MarkupString)SyntaxHighlighter.ToHtml(Text, Language)) +
+
@code { + private bool copied; + private string Text => Code.Trim('\r', '\n').ReplaceLineEndings("\n"); - /// "csharp" for highlighting; any other value renders plain text. + /// csharp, graphql, json, shell or xml are highlighted; any other value renders plain text. [Parameter] public string Language { get; set; } = "csharp"; [Parameter] public string Title { get; set; } /// The code. Pass a raw string literal, Razor would otherwise parse the braces and at signs. [Parameter, EditorRequired] public string Code { get; set; } + + private async Task CopyAsync() + { + await TablerService.CopyToClipboard(Text); + copied = true; + StateHasChanged(); + await Task.Delay(TimeSpan.FromSeconds(2)); + copied = false; + } } diff --git a/docs/Linq2GraphQL.Docs/Shared/CopyCommand.razor b/docs/Linq2GraphQL.Docs/Shared/CopyCommand.razor new file mode 100644 index 00000000..58003fe6 --- /dev/null +++ b/docs/Linq2GraphQL.Docs/Shared/CopyCommand.razor @@ -0,0 +1,25 @@ +@using TabBlazor.Services +@inject TablerService TablerService + +
+ $ + @Command + +
+ +@code { + private bool copied; + + [Parameter, EditorRequired] public string Command { get; set; } + + private async Task CopyAsync() + { + await TablerService.CopyToClipboard(Command); + copied = true; + StateHasChanged(); + await Task.Delay(TimeSpan.FromSeconds(2)); + copied = false; + } +} diff --git a/docs/Linq2GraphQL.Docs/Shared/DocsNav.cs b/docs/Linq2GraphQL.Docs/Shared/DocsNav.cs new file mode 100644 index 00000000..74f6393c --- /dev/null +++ b/docs/Linq2GraphQL.Docs/Shared/DocsNav.cs @@ -0,0 +1,45 @@ +namespace Linq2GraphQL.Docs.Shared; + +public record DocPage(string Href, string Title, string SourceFile); + +public record DocSection(string Title, IReadOnlyList Pages); + +public static class DocsNav +{ + public const string RepositoryUrl = "https://github.com/Linq2GraphQL/Linq2GraphQL.Client"; + public const string NuGetUrl = "https://www.nuget.org/packages/Linq2GraphQL.Client"; + + public static readonly IReadOnlyList Sections = + [ + new("Overview", + [ + new("", "Introduction", "Pages/Index.razor"), + new("getting-started", "Getting started", "Pages/GettingStarted.razor") + ]), + new("Guides", + [ + new("selecting-fields", "Selecting fields", "Pages/SelectingFields.razor"), + new("error-handling", "Error handling", "Pages/ErrorHandling.razor"), + new("paging", "Paging", "Pages/Paging.razor"), + new("batching", "Batching", "Pages/Batching.razor"), + new("subscriptions", "Subscriptions", "Pages/Subscriptions.razor"), + new("agent-mode", "Agent mode", "Pages/AgentMode.razor") + ]), + new("Tools", + [ + new("generator", "Generator reference", "Pages/Generator.razor"), + new("generate-client", "Online generator", "Components/GenerateClient.razor") + ]) + ]; + + public static readonly DocPage[] Pages = Sections.SelectMany(section => section.Pages).ToArray(); + + public static int IndexOf(string relativePath) => + Array.FindIndex(Pages, page => page.Href == NormalizePath(relativePath)); + + public static string NormalizePath(string relativePath) => + relativePath.Split('?', '#')[0].Trim('/'); + + public static string SourceUrl(DocPage page) => + $"{RepositoryUrl}/blob/main/docs/Linq2GraphQL.Docs/{page.SourceFile}"; +} diff --git a/docs/Linq2GraphQL.Docs/Shared/DocsSidebar.razor b/docs/Linq2GraphQL.Docs/Shared/DocsSidebar.razor new file mode 100644 index 00000000..9ff59ae6 --- /dev/null +++ b/docs/Linq2GraphQL.Docs/Shared/DocsSidebar.razor @@ -0,0 +1,24 @@ + + +@code { + [Parameter] public bool Open { get; set; } + + [Parameter] public bool MobileOnly { get; set; } +} diff --git a/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor b/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor index af1844b1..b4a420e5 100644 --- a/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor +++ b/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor @@ -4,14 +4,117 @@ +Skip to content -
- +
+
+ -
-
- @Body + + + Linq2GraphQL + + v@(Version) + + + +
+ + + + + + +
-
+
+ +@if (IsLanding) +{ +
+ +
+ @Body +
+} +else +{ +
+
+ + + +
+
+ @Body +
+ + @if (CurrentPage is not null) + { + + + + } +
+ + +
+} +
+ +
diff --git a/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor.cs b/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor.cs new file mode 100644 index 00000000..5be9fa0f --- /dev/null +++ b/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.JSInterop; + +namespace Linq2GraphQL.Docs.Shared; + +public partial class MainLayout : IDisposable +{ + private List headings = []; + private bool menuOpen; + private bool observeHeadingsAfterRender; + private string pendingFragment; + private bool refreshHeadings = true; + private string theme = "light"; + + [Inject] private NavigationManager Navigation { get; set; } + [Inject] private IJSRuntime JS { get; set; } + + private static string Version => ThisAssembly.AssemblyInformationalVersion.Split('+')[0]; + + private string CurrentPath => DocsNav.NormalizePath(Navigation.ToBaseRelativePath(Navigation.Uri)); + + private bool IsLanding => CurrentPath == ""; + + private bool IsGuidePage => !IsLanding && CurrentPath != "generate-client"; + + private DocPage CurrentPage => PageAt(DocsNav.IndexOf(CurrentPath)); + + private DocPage PreviousPage => PageAt(DocsNav.IndexOf(CurrentPath) - 1); + + private DocPage NextPage => PageAt(DocsNav.IndexOf(CurrentPath) is var index and >= 0 ? index + 1 : -1); + + public void Dispose() => Navigation.LocationChanged -= OnLocationChanged; + + protected override void OnInitialized() + { + pendingFragment = FragmentOf(Navigation.Uri); + Navigation.LocationChanged += OnLocationChanged; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + theme = await JS.InvokeAsync("l2gDocs.getTheme"); + } + + if (refreshHeadings) + { + refreshHeadings = false; + headings = await JS.InvokeAsync>("l2gDocs.readHeadings"); + observeHeadingsAfterRender = true; + StateHasChanged(); + return; + } + + if (observeHeadingsAfterRender) + { + observeHeadingsAfterRender = false; + await JS.InvokeVoidAsync("l2gDocs.observeHeadings"); + if (pendingFragment is not null) + { + await JS.InvokeVoidAsync("l2gDocs.scrollToHeading", pendingFragment); + pendingFragment = null; + } + } + } + + private void OnLocationChanged(object sender, LocationChangedEventArgs e) => + _ = InvokeAsync(async () => + { + menuOpen = false; + pendingFragment = FragmentOf(e.Location); + refreshHeadings = true; + StateHasChanged(); + if (pendingFragment is null) + { + try + { + await JS.InvokeVoidAsync("l2gDocs.scrollToTop"); + } + catch (JSException) + { + } + } + }); + + private async Task ToggleThemeAsync() + { + theme = theme == "dark" ? "light" : "dark"; + await JS.InvokeVoidAsync("l2gDocs.setTheme", theme); + } + + private void ToggleMenu() => menuOpen = !menuOpen; + + private void CloseMenu() => menuOpen = false; + + private async Task ScrollToHeadingAsync(string id) => + await JS.InvokeVoidAsync("l2gDocs.scrollToHeading", id); + + private static DocPage PageAt(int index) => + index >= 0 && index < DocsNav.Pages.Length ? DocsNav.Pages[index] : null; + + private static string FragmentOf(string uri) + { + var hashIndex = uri.IndexOf('#'); + return hashIndex >= 0 && hashIndex < uri.Length - 1 ? uri[(hashIndex + 1)..] : null; + } + + private record Heading(string Id, string Text); +} diff --git a/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor.css b/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor.css deleted file mode 100644 index 01a5066c..00000000 --- a/docs/Linq2GraphQL.Docs/Shared/MainLayout.razor.css +++ /dev/null @@ -1,81 +0,0 @@ -.page { - position: relative; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; - display: flex; - align-items: center; -} - -.top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; -} - -.top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; -} - -.top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; -} - -@media (max-width: 640.98px) { - .top-row:not(.auth) { - display: none; - } - - .top-row.auth { - justify-content: space-between; - } - - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } -} diff --git a/docs/Linq2GraphQL.Docs/Shared/MainNavigation.razor b/docs/Linq2GraphQL.Docs/Shared/MainNavigation.razor deleted file mode 100644 index a0c49111..00000000 --- a/docs/Linq2GraphQL.Docs/Shared/MainNavigation.razor +++ /dev/null @@ -1,52 +0,0 @@ - - - - Linq2GraphQL -

- Linq2GraphQL -
Version: @ThisAssembly.AssemblyFileVersion
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\ No newline at end of file diff --git a/docs/Linq2GraphQL.Docs/Shared/SyntaxHighlighter.cs b/docs/Linq2GraphQL.Docs/Shared/SyntaxHighlighter.cs new file mode 100644 index 00000000..5de32779 --- /dev/null +++ b/docs/Linq2GraphQL.Docs/Shared/SyntaxHighlighter.cs @@ -0,0 +1,56 @@ +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using ColorCode; + +namespace Linq2GraphQL.Docs.Shared; + +public static partial class SyntaxHighlighter +{ + public static string ToHtml(string code, string language) => language switch + { + "csharp" => new HtmlClassFormatter().GetHtmlString(code, Languages.CSharp), + "xml" => new HtmlClassFormatter().GetHtmlString(code, Languages.Xml), + "graphql" => Wrap(Tokenize(code, GraphQLToken())), + "json" => Wrap(Tokenize(code, JsonToken())), + "shell" => Wrap(Tokenize(code, ShellToken())), + _ => Wrap(WebUtility.HtmlEncode(code)) + }; + + public static string Label(string language) => language switch + { + "csharp" => "C#", + "graphql" => "GraphQL", + "json" => "JSON", + "shell" => "Shell", + "xml" => "XML", + _ => "Text" + }; + + private static string Wrap(string html) => $"
{html}
"; + + private static string Tokenize(string code, Regex tokens) + { + var html = new StringBuilder(); + var position = 0; + foreach (Match match in tokens.Matches(code)) + { + html.Append(WebUtility.HtmlEncode(code[position..match.Index])); + var group = match.Groups.Cast().Skip(1).First(candidate => candidate.Success); + html.Append($"{WebUtility.HtmlEncode(match.Value)}"); + position = match.Index + match.Length; + } + + html.Append(WebUtility.HtmlEncode(code[position..])); + return html.ToString(); + } + + [GeneratedRegex("""(?#[^\n]*)|(?"(?:[^"\\]|\\.)*")|(?\$\w+)|(?\b(?:query|mutation|subscription|fragment|on|true|false|null)\b)|(?\b[A-Z]\w*\b)|(?-?\b\d+(?:\.\d+)?\b)""")] + private static partial Regex GraphQLToken(); + + [GeneratedRegex("""(?"(?:[^"\\]|\\.)*"(?=\s*:))|(?"(?:[^"\\]|\\.)*")|(?\b(?:true|false|null)\b)|(?-?\b\d+(?:\.\d+)?\b)""")] + private static partial Regex JsonToken(); + + [GeneratedRegex("""(?(?:^|(?<=\s))#[^\n]*)|(?"[^"\n]*"|'[^'\n]*')|(?^(?:dotnet|Linq2GraphQL|cd)\b)|(?(?<=\s)--?[\w-]+=?)""", RegexOptions.Multiline)] + private static partial Regex ShellToken(); +} diff --git a/docs/Linq2GraphQL.Docs/wwwroot/apple-touch-icon.png b/docs/Linq2GraphQL.Docs/wwwroot/apple-touch-icon.png index c11958b4..5041fbc0 100644 Binary files a/docs/Linq2GraphQL.Docs/wwwroot/apple-touch-icon.png and b/docs/Linq2GraphQL.Docs/wwwroot/apple-touch-icon.png differ diff --git a/docs/Linq2GraphQL.Docs/wwwroot/css/app.css b/docs/Linq2GraphQL.Docs/wwwroot/css/app.css index 6669fca3..09b295cb 100644 --- a/docs/Linq2GraphQL.Docs/wwwroot/css/app.css +++ b/docs/Linq2GraphQL.Docs/wwwroot/css/app.css @@ -1,111 +1,681 @@ -/*Code*/ -.keyword { - color: #93ddfd; +/* Brand tokens */ +:root, +:root[data-bs-theme=light] { + --l2g-bg: #ffffff; + --l2g-surface: #ffffff; + --l2g-subtle: #f1f5f7; + --l2g-border: #e2e8ec; + --l2g-text: #0e1a24; + --l2g-muted: #4e5d68; + --l2g-primary: #0b4f6c; + --l2g-primary-hover: #073a50; + --l2g-primary-fg: #ffffff; + --l2g-primary-soft: rgba(11, 79, 108, .08); + --l2g-accent: #d97706; + --l2g-accent-text: #b45309; + --l2g-link: #0b5e80; + --l2g-inline-code: #0b4f6c; + --l2g-topbar-bg: rgba(255, 255, 255, .85); + --l2g-mark-hex: #0b4f6c; + --l2g-mark-accent: #d97706; + --l2g-mark-hub: #0b4f6c; + --l2g-code-bg: #0e1a24; + --l2g-code-border: #0e1a24; + --l2g-code-header: #132330; + + --tblr-primary: #0b4f6c; + --tblr-primary-rgb: 11, 79, 108; + --tblr-primary-fg: #ffffff; + --tblr-primary-darken: #073a50; + --tblr-primary-lt: #e7eff2; + --tblr-primary-lt-rgb: 231, 239, 242; + --tblr-link-color: #0b5e80; + --tblr-link-color-rgb: 11, 94, 128; + --tblr-link-hover-color: #073a50; + --tblr-link-hover-color-rgb: 7, 58, 80; + --tblr-body-bg: #ffffff; + --tblr-body-bg-rgb: 255, 255, 255; + --tblr-body-color: #0e1a24; + --tblr-body-color-rgb: 14, 26, 36; + --tblr-bg-surface: #ffffff; + --tblr-bg-surface-secondary: #f1f5f7; + --tblr-border-color: #e2e8ec; + --tblr-secondary: #4e5d68; + --tblr-secondary-rgb: 78, 93, 104; + --tblr-secondary-color: #4e5d68; + --tblr-font-sans-serif: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; + --tblr-font-monospace: "JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace; + --tblr-border-radius: 8px; + color-scheme: light; +} + +:root[data-bs-theme=dark] { + --l2g-bg: #0a131a; + --l2g-surface: #0f1b24; + --l2g-subtle: #14232e; + --l2g-border: #1f303c; + --l2g-text: #e6edf2; + --l2g-muted: #9aabb8; + --l2g-primary: #5cc8e8; + --l2g-primary-hover: #8ad8ef; + --l2g-primary-fg: #06121a; + --l2g-primary-soft: rgba(92, 200, 232, .1); + --l2g-accent: #ffb938; + --l2g-accent-text: #ffb938; + --l2g-link: #7dd3f0; + --l2g-inline-code: #8ad8ef; + --l2g-topbar-bg: rgba(10, 19, 26, .85); + --l2g-mark-hex: #5cc8e8; + --l2g-mark-accent: #ffb938; + --l2g-mark-hub: #1a6e92; + --l2g-code-bg: #0f1b24; + --l2g-code-border: #1f303c; + --l2g-code-header: #132330; + + --tblr-primary: #5cc8e8; + --tblr-primary-rgb: 92, 200, 232; + --tblr-primary-fg: #06121a; + --tblr-primary-darken: #8ad8ef; + --tblr-primary-lt: #12303d; + --tblr-primary-lt-rgb: 18, 48, 61; + --tblr-link-color: #7dd3f0; + --tblr-link-color-rgb: 125, 211, 240; + --tblr-link-hover-color: #a9e3f5; + --tblr-link-hover-color-rgb: 169, 227, 245; + --tblr-body-bg: #0a131a; + --tblr-body-bg-rgb: 10, 19, 26; + --tblr-body-color: #e6edf2; + --tblr-body-color-rgb: 230, 237, 242; + --tblr-bg-surface: #0f1b24; + --tblr-bg-surface-secondary: #14232e; + --tblr-bg-forms: #0f1b24; + --tblr-border-color: #1f303c; + --tblr-secondary: #9aabb8; + --tblr-secondary-rgb: 154, 171, 184; + --tblr-secondary-color: #9aabb8; + color-scheme: dark; } -.number { - color: #ff8383; +/* Base */ +body { + background: var(--l2g-bg); + color: var(--l2g-text); + font-family: var(--tblr-font-sans-serif); + font-size: 15px; + line-height: 1.65; + -webkit-font-smoothing: antialiased; } -/* FocusOnNavigate focuses the page's h1 for screen readers; don't draw a focus ring around it. */ h1:focus { outline: none; } -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; +code, +pre { + font-variant-ligatures: none; } -#blazor-error-ui .dismiss { - cursor: pointer; +::selection { + background: rgba(217, 119, 6, .25); +} + +.docs-skip-link { position: absolute; - right: 0.75rem; - top: 0.5rem; + left: -9999px; + top: 8px; + z-index: 2000; + padding: 8px 14px; + border-radius: 8px; + background: var(--l2g-primary); + color: var(--l2g-primary-fg); } -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; +.docs-skip-link:focus { + left: 12px; } -.blazor-error-boundary::after { - content: "An error has occurred." +/* Brand mark */ +.brand-mark-hex { + stroke: var(--l2g-mark-hex); } -.loading-progress { - position: relative; +.brand-mark-edge { + stroke: var(--l2g-mark-accent); +} + +.brand-mark-node { + fill: var(--l2g-mark-accent); +} + +.brand-mark-hub { + fill: var(--l2g-mark-hub); +} + +/* Loading screen */ +.app-loading { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 24px; +} + +.app-loading-bar { + width: 160px; + height: 3px; + border-radius: 3px; + background: linear-gradient(to right, #d97706 var(--blazor-load-percentage, 0%), var(--l2g-border) var(--blazor-load-percentage, 0%)); +} + +/* Top bar */ +.docs-topbar { + position: sticky; + top: 0; + z-index: 1030; + height: 60px; + background: var(--l2g-topbar-bg); + backdrop-filter: saturate(180%) blur(12px); + border-bottom: 1px solid var(--l2g-border); +} + +.docs-topbar-inner { + max-width: 1440px; + height: 100%; + margin: 0 auto; + padding: 0 24px; + display: flex; + align-items: center; + gap: 12px; +} + +.docs-brand { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--l2g-text); + text-decoration: none; +} + +.docs-brand:hover { + color: var(--l2g-text); + text-decoration: none; +} + +.docs-wordmark { + font-family: "Space Grotesk", var(--tblr-font-sans-serif); + font-size: 19px; + font-weight: 700; + letter-spacing: -.02em; +} + +.docs-wordmark-accent { + color: var(--l2g-accent-text); +} + +.docs-version { + font-family: var(--tblr-font-monospace); + font-size: 11px; + color: var(--l2g-muted); + border: 1px solid var(--l2g-border); + border-radius: 999px; + padding: 1px 8px; + white-space: nowrap; +} + +.docs-topnav { + display: flex; + gap: 2px; + margin-left: 20px; +} + +.docs-topnav a { + padding: 6px 12px; + border-radius: 8px; + color: var(--l2g-muted); + font-size: 14.5px; + font-weight: 500; + text-decoration: none; +} + +.docs-topnav a:hover { + color: var(--l2g-text); + background: var(--l2g-subtle); +} + +.docs-topnav a.active { + color: var(--l2g-text); + background: var(--l2g-subtle); +} + +.docs-topbar-actions { + margin-left: auto; + display: flex; + gap: 2px; +} + +.docs-icon-button { + width: 40px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--l2g-muted); + cursor: pointer; +} + +.docs-icon-button:hover { + color: var(--l2g-text); + background: var(--l2g-subtle); +} + +.docs-icon-button .icon { + width: 20px; + height: 20px; +} + +.docs-menu-button { + display: none; + margin-left: -8px; +} + +/* Docs layout */ +.docs-layout { + max-width: 1440px; + margin: 0 auto; + padding: 0 24px; + display: grid; + grid-template-columns: 248px minmax(0, 1fr) 220px; + gap: 48px; +} + +.docs-sidebar { + position: sticky; + top: 60px; + height: calc(100vh - 60px); + overflow-y: auto; + padding: 32px 4px 48px 0; +} + +.docs-nav-section + .docs-nav-section { + margin-top: 28px; +} + +.docs-nav-heading { + margin: 0 0 6px 12px; + font-size: 12px; + font-weight: 600; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--l2g-muted); +} + +.docs-nav-section ul, +.docs-toc ul { + list-style: none; + margin: 0; + padding: 0; +} + +.docs-nav-link { display: block; - width: 8rem; - height: 8rem; - margin: 20vh auto 1rem auto; + padding: 6px 12px; + border-radius: 8px; + color: var(--l2g-muted); + font-size: 14.5px; + text-decoration: none; } -.loading-progress circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.6rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); +.docs-nav-link:hover { + color: var(--l2g-text); + background: var(--l2g-subtle); + text-decoration: none; } -.loading-progress circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; +.docs-nav-link.active { + color: var(--l2g-primary); + background: var(--l2g-primary-soft); + font-weight: 600; } -.loading-progress-text { - position: absolute; - text-align: center; - font-weight: bold; - inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +.docs-main { + min-width: 0; + padding: 40px 0 72px; } -.loading-progress-text:after { - content: var(--blazor-load-percentage-text, "Loading"); +.docs-toc { + position: sticky; + top: 60px; + align-self: start; + max-height: calc(100vh - 60px); + overflow-y: auto; + padding: 40px 0; } -/*Documentation pages*/ -.doc-page { - padding-bottom: 3rem; +.docs-toc-heading { + margin-bottom: 10px; + font-size: 12px; + font-weight: 600; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--l2g-muted); +} + +.docs-toc a { + display: block; + padding: 4px 0 4px 14px; + border-left: 2px solid var(--l2g-border); + color: var(--l2g-muted); + font-size: 13.5px; + line-height: 1.45; + text-decoration: none; } -.hero { - padding: 2.5rem 1rem 1.5rem; +.docs-toc a:hover { + color: var(--l2g-text); } -.hero .lead { - max-width: 640px; +.docs-toc a.active { + color: var(--l2g-text); + border-left-color: var(--l2g-accent); + font-weight: 500; +} + +.docs-backdrop, +.docs-sidebar.mobile-only { + display: none; +} + +.docs-page-meta { + max-width: 780px; + margin-top: 56px; + font-size: 14px; +} + +.docs-page-meta a { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--l2g-muted); + text-decoration: none; +} + +.docs-page-meta a:hover { + color: var(--l2g-link); +} + +.docs-page-meta .icon { + width: 16px; + height: 16px; +} + +.docs-pager { + max-width: 780px; + margin-top: 20px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.docs-pager-link { + display: flex; + flex-direction: column; + gap: 2px; + padding: 14px 18px; + border: 1px solid var(--l2g-border); + border-radius: 12px; + color: var(--l2g-text); + text-decoration: none; +} + +.docs-pager-link:hover { + border-color: var(--l2g-primary); + text-decoration: none; + color: var(--l2g-text); +} + +.docs-pager-link.next { + grid-column: 2; + text-align: right; + align-items: flex-end; +} + +.docs-pager-label { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: var(--l2g-muted); +} + +.docs-pager-label .icon { + width: 14px; + height: 14px; +} + +.docs-pager-title { + font-weight: 600; + color: var(--l2g-link); +} + +/* Footer */ +.docs-footer { + border-top: 1px solid var(--l2g-border); +} + +.docs-footer-inner { + max-width: 1440px; + margin: 0 auto; + padding: 28px 24px; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 16px; + font-size: 14px; + color: var(--l2g-muted); +} + +.docs-footer-brand { + display: flex; + align-items: center; + gap: 10px; +} + +.docs-footer-links { + display: flex; + gap: 20px; +} + +.docs-footer-links a { + color: var(--l2g-muted); +} + +.docs-footer-links a:hover { + color: var(--l2g-text); +} + +@media (max-width: 1279.98px) { + .docs-layout { + grid-template-columns: 232px minmax(0, 1fr); + gap: 40px; + } + + .docs-toc { + display: none; + } +} + +@media (max-width: 991.98px) { + .docs-menu-button { + display: inline-flex; + } + + .docs-layout { + grid-template-columns: minmax(0, 1fr); + gap: 0; + padding: 0 20px; + } + + .docs-sidebar { + position: fixed; + top: 60px; + left: 0; + bottom: 0; + z-index: 1025; + width: 288px; + height: auto; + padding: 24px 16px 48px; + background: var(--l2g-bg); + border-right: 1px solid var(--l2g-border); + transform: translateX(-100%); + transition: transform .2s ease; + } + + .docs-sidebar.mobile-only { + display: block; + } + + .docs-sidebar.open { + transform: translateX(0); + } + + .docs-backdrop.show { + display: block; + position: fixed; + inset: 60px 0 0 0; + z-index: 1020; + background: rgba(6, 18, 26, .45); + } + + .docs-main { + padding-top: 28px; + } +} + +@media (max-width: 767.98px) { + .docs-topbar-inner { + padding: 0 16px; + gap: 8px; + } + + .docs-topnav, + .docs-version { + display: none; + } + + .docs-layout { + padding: 0 16px; + } + + .docs-pager { + grid-template-columns: minmax(0, 1fr); + } + + .docs-pager-link.next { + grid-column: 1; + } +} + +/* Documentation content */ +.doc-content { + max-width: 780px; +} + +.doc-content.wide, +.doc-content:has(.doc-wide) { + max-width: none; } .page-heading { - font-size: 1.75rem; - margin-top: 1rem; + margin: 0 0 12px; + font-family: "Space Grotesk", var(--tblr-font-sans-serif); + font-size: clamp(2rem, 4vw, 2.5rem); + font-weight: 700; + line-height: 1.15; + letter-spacing: -.025em; + color: var(--l2g-text); +} + +.page-heading .badge { + font-family: var(--tblr-font-sans-serif); + font-size: 12px; + letter-spacing: 0; + vertical-align: middle; +} + +.doc-content .lead { + font-size: 18px; + line-height: 1.6; + color: var(--l2g-muted) !important; } .doc-section { - margin-top: 2.5rem; + margin-top: 48px; } -.doc-section h2 { +.doc-content h2 { display: flex; align-items: center; - gap: .5rem; - font-size: 1.35rem; - margin-bottom: 1rem; + gap: 12px; + margin: 48px 0 14px; + font-family: "Space Grotesk", var(--tblr-font-sans-serif); + font-size: 25px; + font-weight: 600; + letter-spacing: -.015em; + line-height: 1.3; + color: var(--l2g-text); + scroll-margin-top: 84px; +} + +.doc-section > h2:first-child { + margin-top: 0; +} + +.doc-content h3 { + margin: 28px 0 10px; + font-size: 17px; + font-weight: 600; + color: var(--l2g-text); } -.doc-section .table-responsive { - margin-bottom: 1rem; +.doc-content p, +.doc-content li { + color: var(--l2g-text); +} + +.doc-content p { + margin-bottom: 16px; +} + +.doc-content a:not(.btn):not(.docs-pager-link) { + color: var(--l2g-link); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--l2g-link) 35%, transparent); + text-underline-offset: 3px; +} + +.doc-content a:not(.btn):not(.docs-pager-link):hover { + text-decoration-color: currentColor; +} + +.doc-content :not(pre) > code, +.docs-landing :not(pre) > code:not(.copy-command-text) { + padding: .12em .4em; + border: 1px solid var(--l2g-border); + border-radius: 6px; + background: var(--l2g-subtle); + color: var(--l2g-inline-code); + font-family: var(--tblr-font-monospace); + font-size: .86em; + white-space: nowrap; +} + +.doc-content td code { + white-space: normal; } .step-number { @@ -113,56 +683,596 @@ h1:focus { align-items: center; justify-content: center; flex: none; - width: 1.75rem; - height: 1.75rem; + width: 30px; + height: 30px; border-radius: 50%; - background: var(--tblr-primary); - color: #fff; - font-size: .875rem; + background: var(--l2g-primary); + color: var(--l2g-primary-fg); + font-family: var(--tblr-font-sans-serif); + font-size: 14px; font-weight: 600; - margin-bottom: .5rem; + margin-bottom: 10px; } -.doc-section h2 .step-number { +.doc-content h2 .step-number { margin-bottom: 0; } +.doc-content .table-responsive { + margin-bottom: 20px; + border: 1px solid var(--l2g-border); + border-radius: 12px; +} + +.doc-content .table-responsive .table { + margin: 0; + border: 0 !important; + font-size: 14.5px; +} + +.doc-content .table th { + background: var(--l2g-subtle); + color: var(--l2g-muted); + font-size: 12px; + font-weight: 600; + letter-spacing: .06em; + text-transform: uppercase; +} + +.doc-content .table td, +.doc-content .table th { + padding: 10px 14px; + border-color: var(--l2g-border); +} + +.doc-content .table tr:last-child td { + border-bottom: 0; +} + +.card { + border-color: var(--l2g-border); + border-radius: 12px; + box-shadow: none; +} + +/* Code blocks */ .code-block { - margin-bottom: 1rem; - border-radius: 4px; + margin: 0 0 20px; + border: 1px solid var(--l2g-code-border); + border-radius: 12px; overflow: hidden; - background: #182433; + background: var(--l2g-code-bg); } -.code-block-title { - padding: .375rem .75rem; - font-size: .75rem; - font-weight: 600; +.code-block-header { + display: flex; + align-items: center; + gap: 10px; + min-height: 40px; + padding: 6px 8px 6px 16px; + background: var(--l2g-code-header); + border-bottom: 1px solid rgba(255, 255, 255, .06); +} + +.code-block-language { + font-family: var(--tblr-font-monospace); + font-size: 11px; + font-weight: 500; + letter-spacing: .06em; text-transform: uppercase; - letter-spacing: .04em; - color: #9ba9be; - border-bottom: 1px solid #2c3b4f; + color: #ffb938; +} + +.code-block-title { + font-size: 12.5px; + color: #9aabb8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.code-block-copy { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 9px; + border: 1px solid rgba(255, 255, 255, .1); + border-radius: 7px; + background: transparent; + color: #c5d2db; + font-size: 12px; + cursor: pointer; +} + +.code-block-copy:hover { + background: rgba(255, 255, 255, .06); + color: #ffffff; +} + +.code-block-copy.copied { + color: #8be0a4; + border-color: rgba(139, 224, 164, .35); +} + +.code-block-copy .icon { + width: 14px; + height: 14px; +} + +.code-block-body > div { + background: transparent !important; + color: inherit !important; } .code-block pre { margin: 0; - padding: .75rem; + padding: 16px 18px; + border: 0; border-radius: 0; - background: transparent; - color: #e6e9ee; - font-size: .8125rem; - line-height: 1.5; + background: transparent !important; + color: #e3eaf0 !important; + font-family: var(--tblr-font-monospace); + font-size: 13.5px; + line-height: 1.65; overflow-x: auto; + white-space: pre; +} + +.code-block .keyword, +.code-block .xmlName { + color: #7dd3f0 !important; +} + +.code-block .string, +.code-block .xmlAttributeValue { + color: #b9e28c !important; +} + +.code-block .comment, +.code-block .xmlComment, +.code-block .xmlDelimiter, +.code-block .xmlAttributeQuotes { + color: #71879a !important; } .code-block .comment { - color: #8b9bb0; + font-style: italic; } -.code-block .string { - color: #a5d6a7; +.code-block .number { + color: #f7a072 !important; } .code-block .className { - color: #ffe484; + color: #ffb938 !important; +} + +.code-block .variable { + color: #f0a6ca !important; +} + +.code-block .property, +.code-block .xmlAttribute { + color: #9cdcfe !important; +} + +/* Landing page */ +.landing-hero { + position: relative; + max-width: 1240px; + margin: 0 auto; + padding: 80px 24px 64px; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); + gap: 56px; + align-items: center; +} + +.landing-eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 4px 12px; + border: 1px solid var(--l2g-border); + border-radius: 999px; + background: var(--l2g-subtle); + color: var(--l2g-muted); + font-size: 13px; + font-weight: 500; +} + +.landing-eyebrow-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--l2g-accent); +} + +.landing-title { + margin: 20px 0 18px; + font-family: "Space Grotesk", var(--tblr-font-sans-serif); + font-size: clamp(2.5rem, 5.2vw, 3.9rem); + font-weight: 700; + line-height: 1.04; + letter-spacing: -.035em; + color: var(--l2g-text); +} + +.landing-title-accent { + color: var(--l2g-accent-text); +} + +.landing-lead { + max-width: 560px; + margin-bottom: 28px; + font-size: 18.5px; + line-height: 1.6; + color: var(--l2g-muted); +} + +.landing-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.landing-actions .btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 18px; + border-radius: 10px; + font-size: 15px; + font-weight: 600; +} + +.landing-actions .btn .icon { + width: 18px; + height: 18px; +} + +.landing-install { + margin-top: 28px; + max-width: 480px; +} + +.copy-command { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 6px 6px 14px; + border: 1px solid var(--l2g-border); + border-radius: 10px; + background: var(--l2g-subtle); + font-family: var(--tblr-font-monospace); + font-size: 13.5px; +} + +.copy-command-prompt { + color: var(--l2g-accent-text); + user-select: none; +} + +.copy-command-text { + flex: 1; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; + white-space: nowrap; + color: var(--l2g-text); +} + +.copy-command .docs-icon-button { + width: 32px; + height: 32px; + flex: none; +} + +.copy-command .docs-icon-button .icon { + width: 16px; + height: 16px; +} + +.landing-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} + +.landing-visual { + position: relative; + min-width: 0; +} + +.landing-visual .code-block { + position: relative; + margin: 0; + box-shadow: 0 24px 48px -24px rgba(6, 18, 26, .45); +} + +.landing-visual .code-block + .code-block { + margin: -28px 0 0 48px; +} + +.landing-section { + max-width: 1240px; + margin: 0 auto; + padding: 56px 24px; +} + +.landing-section + .landing-section { + border-top: 1px solid var(--l2g-border); +} + +.landing-section-header { + max-width: 720px; + margin-bottom: 32px; +} + +.landing-section-header h2 { + margin: 0 0 12px; + font-family: "Space Grotesk", var(--tblr-font-sans-serif); + font-size: clamp(1.75rem, 3vw, 2.25rem); + font-weight: 700; + letter-spacing: -.025em; + color: var(--l2g-text); +} + +.landing-section-header p { + margin: 0; + font-size: 17px; + color: var(--l2g-muted); +} + +.landing-prose { + max-width: 780px; + font-size: 16px; +} + +.landing-prose p { + color: var(--l2g-text); +} + +.landing-section a:not(.btn):not(.feature-card) { + color: var(--l2g-link); +} + +.landing-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; +} + +.landing-code-pair { + display: grid; + grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); + gap: 20px; + align-items: start; +} + +.landing-code-pair .code-block { + margin: 0; +} + +.landing-note { + max-width: 780px; + margin-top: 20px; + color: var(--l2g-muted); +} + +.feature-card { + display: flex; + flex-direction: column; + gap: 10px; + padding: 24px; + border: 1px solid var(--l2g-border); + border-radius: 14px; + background: var(--l2g-surface); + color: var(--l2g-text); + text-decoration: none; +} + +a.feature-card:hover { + border-color: var(--l2g-primary); + color: var(--l2g-text); + text-decoration: none; +} + +.feature-card h3 { + margin: 0; + font-size: 17px; + font-weight: 600; +} + +.feature-card p { + margin: 0; + color: var(--l2g-muted); + font-size: 14.5px; + line-height: 1.6; +} + +.feature-icon { + width: 40px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 10px; + background: var(--l2g-primary-soft); + color: var(--l2g-primary); +} + +.feature-icon .icon { + width: 22px; + height: 22px; +} + +.feature-link { + margin-top: auto; + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--l2g-link); + font-size: 14px; + font-weight: 500; +} + +.feature-link .icon { + width: 15px; + height: 15px; +} + +.package-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.package-card { + display: flex; + gap: 14px; + padding: 18px 20px; + border: 1px solid var(--l2g-border); + border-radius: 14px; + color: var(--l2g-text); + text-decoration: none; +} + +.package-card:hover { + border-color: var(--l2g-primary); + color: var(--l2g-text); + text-decoration: none; +} + +.package-card-name { + font-family: var(--tblr-font-monospace); + font-size: 14px; + font-weight: 500; + color: var(--l2g-link); +} + +.package-card p { + margin: 4px 0 0; + font-size: 14.5px; + color: var(--l2g-muted); +} + +.landing-cta { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 36px 40px; + border-radius: 18px; + background: #0b4f6c; + color: #ffffff; +} + +:root[data-bs-theme=dark] .landing-cta { + background: #0f2a38; + border: 1px solid #1f3f50; +} + +.landing-cta h2 { + margin: 0 0 6px; + font-family: "Space Grotesk", var(--tblr-font-sans-serif); + font-size: 26px; + font-weight: 700; + color: #ffffff; +} + +.landing-cta p { + margin: 0; + color: #c7dde6; +} + +.landing-cta .btn { + background: #ffb938; + border-color: #ffb938; + color: #0e1a24; +} + +.landing-cta .btn:hover { + background: #ffc75e; + border-color: #ffc75e; + color: #0e1a24; +} + +.landing-credits { + margin-top: 28px; + font-size: 14px; + color: var(--l2g-muted); +} + +@media (max-width: 991.98px) { + .landing-hero { + grid-template-columns: minmax(0, 1fr); + padding-top: 48px; + gap: 40px; + } + + .landing-grid, + .landing-code-pair { + grid-template-columns: minmax(0, 1fr); + } + + .landing-visual .code-block + .code-block { + margin: 16px 0 0; + } + +} + +@media (min-width: 768px) and (max-width: 991.98px) { + .landing-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 767.98px) { + .landing-hero, + .landing-section { + padding-left: 16px; + padding-right: 16px; + } + + .package-list { + grid-template-columns: minmax(0, 1fr); + } + + .landing-cta { + padding: 28px 24px; + } +} + +/* Blazor */ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + +.blazor-error-boundary::after { + content: "An error has occurred." } diff --git a/docs/Linq2GraphQL.Docs/wwwroot/favicon-16x16.png b/docs/Linq2GraphQL.Docs/wwwroot/favicon-16x16.png index fe0cab2b..9ec5e8f8 100644 Binary files a/docs/Linq2GraphQL.Docs/wwwroot/favicon-16x16.png and b/docs/Linq2GraphQL.Docs/wwwroot/favicon-16x16.png differ diff --git a/docs/Linq2GraphQL.Docs/wwwroot/favicon-32x32.png b/docs/Linq2GraphQL.Docs/wwwroot/favicon-32x32.png index 56fd62a7..371dd503 100644 Binary files a/docs/Linq2GraphQL.Docs/wwwroot/favicon-32x32.png and b/docs/Linq2GraphQL.Docs/wwwroot/favicon-32x32.png differ diff --git a/docs/Linq2GraphQL.Docs/wwwroot/favicon.ico b/docs/Linq2GraphQL.Docs/wwwroot/favicon.ico index a1b0d417..96023f7e 100644 Binary files a/docs/Linq2GraphQL.Docs/wwwroot/favicon.ico and b/docs/Linq2GraphQL.Docs/wwwroot/favicon.ico differ diff --git a/docs/Linq2GraphQL.Docs/wwwroot/img/logo-tile.svg b/docs/Linq2GraphQL.Docs/wwwroot/img/logo-tile.svg new file mode 100644 index 00000000..9d3186ca --- /dev/null +++ b/docs/Linq2GraphQL.Docs/wwwroot/img/logo-tile.svg @@ -0,0 +1,22 @@ + + Linq2GraphQL + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Linq2GraphQL.Docs/wwwroot/img/logo.svg b/docs/Linq2GraphQL.Docs/wwwroot/img/logo.svg index 3a34881b..4386c2a2 100644 --- a/docs/Linq2GraphQL.Docs/wwwroot/img/logo.svg +++ b/docs/Linq2GraphQL.Docs/wwwroot/img/logo.svg @@ -1,22 +1,31 @@ - + Linq2GraphQL - - - + + + + + + - - - - - - - - - - - - - - + + + + + + + + + diff --git a/docs/Linq2GraphQL.Docs/wwwroot/img/og-image.png b/docs/Linq2GraphQL.Docs/wwwroot/img/og-image.png new file mode 100644 index 00000000..2d72334a Binary files /dev/null and b/docs/Linq2GraphQL.Docs/wwwroot/img/og-image.png differ diff --git a/docs/Linq2GraphQL.Docs/wwwroot/index.html b/docs/Linq2GraphQL.Docs/wwwroot/index.html index b70330b8..0777444d 100644 --- a/docs/Linq2GraphQL.Docs/wwwroot/index.html +++ b/docs/Linq2GraphQL.Docs/wwwroot/index.html @@ -3,10 +3,51 @@ - + Linq2GraphQL Documentation + + + + + + + + + + + + + + + + + + + @@ -15,19 +56,19 @@ - + +
- - - - -
+
+ +
+
@@ -36,6 +77,7 @@ 🗙
+ diff --git a/docs/Linq2GraphQL.Docs/wwwroot/js/docs.js b/docs/Linq2GraphQL.Docs/wwwroot/js/docs.js new file mode 100644 index 00000000..dd037af4 --- /dev/null +++ b/docs/Linq2GraphQL.Docs/wwwroot/js/docs.js @@ -0,0 +1,76 @@ +window.l2gDocs = (() => { + const themeKey = "l2g-theme"; + let updateScheduled = false; + + function getTheme() { + return document.documentElement.getAttribute("data-bs-theme") === "dark" ? "dark" : "light"; + } + + function setTheme(theme) { + document.documentElement.setAttribute("data-bs-theme", theme); + try { + localStorage.setItem(themeKey, theme); + } catch { + } + } + + function readHeadings() { + return Array.from(document.querySelectorAll(".doc-content h2[id]")) + .map(heading => ({ id: heading.id, text: headingText(heading) })); + } + + function headingText(heading) { + const copy = heading.cloneNode(true); + copy.querySelectorAll(".step-number, .badge").forEach(element => element.remove()); + return copy.textContent.trim(); + } + + function markActiveHeading(id) { + document.querySelectorAll(".docs-toc a[data-heading]").forEach(link => { + link.classList.toggle("active", link.dataset.heading === id); + }); + } + + function updateActiveHeading() { + const headings = Array.from(document.querySelectorAll(".doc-content h2[id]")); + if (headings.length === 0) { + return; + } + + const atBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2; + const passed = headings.filter(heading => heading.getBoundingClientRect().top <= 120); + const current = atBottom ? headings[headings.length - 1] : passed[passed.length - 1] ?? headings[0]; + markActiveHeading(current.id); + } + + function onScroll() { + if (!updateScheduled) { + updateScheduled = true; + requestAnimationFrame(() => { + updateScheduled = false; + updateActiveHeading(); + }); + } + } + + function observeHeadings() { + window.removeEventListener("scroll", onScroll); + window.addEventListener("scroll", onScroll, { passive: true }); + updateActiveHeading(); + } + + function scrollToHeading(id) { + const heading = document.getElementById(id); + if (heading) { + heading.scrollIntoView({ behavior: "smooth", block: "start" }); + history.replaceState(null, "", `${location.pathname}#${id}`); + markActiveHeading(id); + } + } + + function scrollToTop() { + window.scrollTo({ top: 0 }); + } + + return { getTheme, setTheme, readHeadings, observeHeadings, scrollToHeading, scrollToTop }; +})(); diff --git a/icon.png b/icon.png index 07ee3269..7c7d27f6 100644 Binary files a/icon.png and b/icon.png differ