mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-13 12:47:21 +00:00
Changed all icons of AI Studio to Lucide (#1187)
This commit is contained in:
1 parent
9a676101f7
commit
7ef26cd5c8
352 files changed
+6261
-1620
No files matched your search
@@ -1,4 +1,10 @@
|
||||
; Unshipped analyzer release
|
||||
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||
|
||||
### New Rules
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
---------|------------------|----------|---------------------
|
||||
LUC001 | SourceGeneration | Info | LucideIconGenerator
|
||||
LUC002 | SourceGeneration | Warning | LucideIconGenerator
|
||||
LUC003 | SourceGeneration | Warning | LucideIconGenerator
|
||||
@@ -0,0 +1,46 @@
|
||||
# Lucide
|
||||
|
||||
AI Studio draws its icons with [Lucide](https://lucide.dev), taken from the npm package [`lucide-static`](https://www.npmjs.com/package/lucide-static), version **1.71.0**. The app uses the icon font of that package, not single SVG files: one font holds every icon, and MudBlazor shows a font glyph wherever it accepts an icon.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Taken from the package | Purpose |
|
||||
|---|---|---|
|
||||
| `app/SourceGeneratedMappings/Lucide/codepoints.json` | `font/codepoints.json` | Maps the name of every icon, aliases included, to its code point in the font. |
|
||||
| `app/MindWork AI Studio/wwwroot/fonts/lucide.woff2` | `font/lucide.woff2` | The font the app ships, declared in `wwwroot/app.css`. |
|
||||
| `app/MindWork AI Studio/wwwroot/fonts/LICENSE-lucide.txt` | `LICENSE` | The license, which has to travel with every copy of the font. |
|
||||
|
||||
All three files are copies without any change. The code points belong to one release of the font, so always replace the three files together.
|
||||
|
||||
## Using the icons
|
||||
|
||||
While the app is built, `LucideIconGenerator` turns `codepoints.json` into two classes in the namespace `AIStudio.Tools`:
|
||||
|
||||
- `LucideIcons` holds a constant for every name, such as `LucideIcons.HOUSE`. Its value is `lucide/` followed by the glyph of the icon: MudBlazor renders such a string as a span with the class `lucide` and the glyph as its text, and `wwwroot/app.css` applies the font to that class.
|
||||
- `LucideIconRegistry` finds an icon by the name lucide.dev shows for it, such as `house`. Assistant plugins name their icons this way.
|
||||
|
||||
Besides the current names, `codepoints.json` holds aliases and names Lucide has deprecated, such as `home` for `house` or `trash-2` for `trash`, so that a plugin written with an older name keeps working. In the app itself, always pick the current name as lucide.dev shows it.
|
||||
|
||||
The app takes its icons only from `AppIcons` in `app/MindWork AI Studio/Tools/AppIcons.cs`. It names every icon after its meaning in AI Studio, such as `AppIcons.DELETE`, and decides there which Lucide icon stands for it. To use an icon, take the constant of its meaning from `AppIcons`; when there is none yet, add one there. Never use `LucideIcons` or the icons of MudBlazor anywhere else.
|
||||
|
||||
## License
|
||||
|
||||
Lucide is licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE). The icons Lucide derived from [Feather](https://github.com/feathericons/feather) are licensed under the MIT License; `LICENSE-lucide.txt` lists them.
|
||||
|
||||
## Updating
|
||||
|
||||
1. Download and unpack the package of the new version:
|
||||
|
||||
```bash
|
||||
curl -L -o lucide-static.tgz "https://registry.npmjs.org/lucide-static/-/lucide-static-<version>.tgz"
|
||||
tar -xzf lucide-static.tgz
|
||||
```
|
||||
|
||||
2. Copy the three files from `package/` to the places in the table above.
|
||||
3. Change the version at the top of this file.
|
||||
4. Build the app and run the tests.
|
||||
5. A few places show Lucide icons as images, where no web font works, and hold copies of their SVG files from `package/icons/`. Check whether these icons changed in the new version, and take them anew if so:
|
||||
- `cpu-ai` and `server` in `wwwroot/images/provider-icons/`, see the README there,
|
||||
- `plug` as the default icon of plugins, `DEFAULT_ICON_SVG` in `Tools/PluginSystem/PluginBase.Icon.cs`,
|
||||
- `settings` in the `icon.lua` of `Plugins/assistants/` and `Plugins/configuration/`.
|
||||
6. Generate the banner of the start page anew, since its icons come from the font, see `media/Banner/README.md`.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace SourceGeneratedMappings;
|
||||
|
||||
/// <summary>
|
||||
/// Turns the code points of the Lucide icon font into icon strings MudBlazor shows as font icons.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The input is Lucide/codepoints.json, copied unchanged from the npm package lucide-static and handed
|
||||
/// to the compiler as an additional file; Lucide/README.md describes the files and how to update them.
|
||||
/// MudIcon renders every icon string of the form "class/content" as a span with that class and that
|
||||
/// text. Each icon string is therefore "lucide/" followed by the glyph of its icon, and the CSS class
|
||||
/// lucide in wwwroot/app.css selects the font. The generator writes two classes: LucideIcons with a
|
||||
/// constant for every name, and LucideIconRegistry, which finds an icon by its name at runtime.
|
||||
/// </remarks>
|
||||
[Generator]
|
||||
#pragma warning disable RS1036
|
||||
public sealed class LucideIconGenerator : IIncrementalGenerator
|
||||
#pragma warning restore RS1036
|
||||
{
|
||||
private const string GENERATED_NAMESPACE = "AIStudio.Tools";
|
||||
private const string CODEPOINTS_PATH_SUFFIX = "/Lucide/codepoints.json";
|
||||
private const string FONT_ICON_CLASS = "lucide";
|
||||
|
||||
private static readonly Regex ENTRY = new("\"(?<name>[^\"]*)\"\\s*:\\s*(?<codepoint>[0-9]+)", RegexOptions.CultureInvariant);
|
||||
private static readonly Regex VALID_NAME = new("^[a-z][a-z0-9]*(-[a-z0-9]+)*$", RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly DiagnosticDescriptor CODEPOINTS_MISSING = new(
|
||||
id: "LUC001",
|
||||
title: "The code points of the Lucide font were not found",
|
||||
messageFormat: "No additional file ending in '{0}' was handed to the compiler. LucideIcons and LucideIconRegistry were not generated.",
|
||||
category: "SourceGeneration",
|
||||
DiagnosticSeverity.Info,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
private static readonly DiagnosticDescriptor NO_ICONS_FOUND = new(
|
||||
id: "LUC002",
|
||||
title: "The code points of the Lucide font hold no icons",
|
||||
messageFormat: "The file '{0}' contains no icon. LucideIcons and LucideIconRegistry were not generated.",
|
||||
category: "SourceGeneration",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
private static readonly DiagnosticDescriptor ICON_SKIPPED = new(
|
||||
id: "LUC003",
|
||||
title: "A Lucide icon was skipped",
|
||||
messageFormat: "The Lucide icon '{0}' with the code point {1} was skipped: {2}",
|
||||
category: "SourceGeneration",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var files = context.AdditionalTextsProvider
|
||||
.Where(static file => IsCodepointsFile(file.Path))
|
||||
.Select(static (file, token) => new CodepointsFile(file.Path, file.GetText(token)?.ToString() ?? string.Empty))
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(files, Generate);
|
||||
}
|
||||
|
||||
private static bool IsCodepointsFile(string path) => path.Replace('\\', '/').EndsWith(CODEPOINTS_PATH_SUFFIX, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static void Generate(SourceProductionContext context, ImmutableArray<CodepointsFile> files)
|
||||
{
|
||||
if (files.IsDefaultOrEmpty)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(CODEPOINTS_MISSING, Location.None, CODEPOINTS_PATH_SUFFIX));
|
||||
return;
|
||||
}
|
||||
|
||||
var file = files[0];
|
||||
var icons = new List<IconDefinition>();
|
||||
foreach (Match match in ENTRY.Matches(file.Content))
|
||||
{
|
||||
var name = match.Groups["name"].Value;
|
||||
var codepointText = match.Groups["codepoint"].Value;
|
||||
if (!VALID_NAME.IsMatch(name))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(ICON_SKIPPED, Location.None, name, codepointText, "the name has to start with a lower-case letter and may only hold lower-case letters and digits, separated by single hyphens"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!int.TryParse(codepointText, NumberStyles.None, CultureInfo.InvariantCulture, out var codepoint) || !IsPrivateUse(codepoint))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(ICON_SKIPPED, Location.None, name, codepointText, "the code point lies outside the private use areas of Unicode"));
|
||||
continue;
|
||||
}
|
||||
|
||||
icons.Add(new IconDefinition(name, codepoint));
|
||||
}
|
||||
|
||||
if (icons.Count == 0)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(NO_ICONS_FOUND, Location.None, file.Path));
|
||||
return;
|
||||
}
|
||||
|
||||
icons.Sort(static (left, right) => string.CompareOrdinal(left.Name, right.Name));
|
||||
context.AddSource("LucideIcons.g.cs", SourceText.From(RenderIcons(icons), Encoding.UTF8));
|
||||
context.AddSource("LucideIconRegistry.g.cs", SourceText.From(RenderRegistry(icons), Encoding.UTF8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Icon fonts place their glyphs in the private use areas, where no real character can collide with them.
|
||||
/// </summary>
|
||||
private static bool IsPrivateUse(int codepoint) => codepoint is >= 0xE000 and <= 0xF8FF or >= 0xF0000 and <= 0x10FFFD;
|
||||
|
||||
private static string RenderIcons(IReadOnlyList<IconDefinition> icons)
|
||||
{
|
||||
var namesByCodepoint = icons.ToLookup(static icon => icon.Codepoint, static icon => icon.Name);
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
builder.AppendLine("#nullable enable");
|
||||
builder.AppendLine();
|
||||
builder.Append("namespace ").Append(GENERATED_NAMESPACE).AppendLine(";");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("/// <summary>");
|
||||
builder.AppendLine("/// Every icon of the Lucide font, as an icon string for MudBlazor.");
|
||||
builder.AppendLine("/// </summary>");
|
||||
builder.AppendLine("/// <remarks>");
|
||||
builder.AppendLine("/// Generated by LucideIconGenerator from Lucide/codepoints.json. The app picks its icons in AppIcons,");
|
||||
builder.AppendLine("/// which is the only class that takes them from here.");
|
||||
builder.AppendLine("/// </remarks>");
|
||||
builder.AppendLine("public static class LucideIcons");
|
||||
builder.AppendLine("{");
|
||||
|
||||
for (var index = 0; index < icons.Count; index++)
|
||||
{
|
||||
var icon = icons[index];
|
||||
var sameGlyph = namesByCodepoint[icon.Codepoint].Where(name => name != icon.Name).ToList();
|
||||
|
||||
if (index > 0)
|
||||
builder.AppendLine();
|
||||
|
||||
builder.Append(" /// <summary>The Lucide icon \"").Append(icon.Name).Append('"');
|
||||
if (sameGlyph.Count > 0)
|
||||
builder.Append(", the same glyph as \"").Append(string.Join("\", \"", sameGlyph)).Append('"');
|
||||
|
||||
builder.AppendLine(".</summary>");
|
||||
builder.Append(" public const string ").Append(icon.ConstantName).Append(" = \"").Append(FONT_ICON_CLASS).Append('/').Append(EscapeCodepoint(icon.Codepoint)).AppendLine("\";");
|
||||
}
|
||||
|
||||
builder.AppendLine("}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string RenderRegistry(IReadOnlyList<IconDefinition> icons)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
builder.AppendLine("#nullable enable");
|
||||
builder.AppendLine("using System;");
|
||||
builder.AppendLine("using System.Collections.Generic;");
|
||||
builder.AppendLine();
|
||||
builder.Append("namespace ").Append(GENERATED_NAMESPACE).AppendLine(";");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("/// <summary>");
|
||||
builder.AppendLine("/// Finds a Lucide icon by the name lucide.dev shows for it, such as \"house\".");
|
||||
builder.AppendLine("/// </summary>");
|
||||
builder.AppendLine("/// <remarks>");
|
||||
builder.AppendLine("/// Generated by LucideIconGenerator from Lucide/codepoints.json, for icons which are named at runtime,");
|
||||
builder.AppendLine("/// such as those in assistant plugins. Aliases count as names of their own.");
|
||||
builder.AppendLine("/// </remarks>");
|
||||
builder.AppendLine("public static class LucideIconRegistry");
|
||||
builder.AppendLine("{");
|
||||
builder.Append(" private static readonly IReadOnlyDictionary<string, string> ICONS_BY_NAME = new Dictionary<string, string>(")
|
||||
.Append(icons.Count.ToString(CultureInfo.InvariantCulture))
|
||||
.AppendLine(", StringComparer.Ordinal)");
|
||||
builder.AppendLine(" {");
|
||||
|
||||
foreach (var icon in icons)
|
||||
builder.Append(" [\"").Append(icon.Name).Append("\"] = LucideIcons.").Append(icon.ConstantName).AppendLine(",");
|
||||
|
||||
builder.AppendLine(" };");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine(" /// Looks up an icon by its name, which has to match exactly.");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(" /// <param name=\"name\">The name of the icon, such as \"house\".</param>");
|
||||
builder.AppendLine(" /// <param name=\"icon\">The icon string for MudBlazor, when the name is known.</param>");
|
||||
builder.AppendLine(" /// <returns>True when the font has an icon of that name.</returns>");
|
||||
builder.AppendLine(" public static bool TryGet(string name, out string icon) => ICONS_BY_NAME.TryGetValue(name, out icon!);");
|
||||
builder.AppendLine("}");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeCodepoint(int codepoint) => codepoint <= 0xFFFF
|
||||
? "\\u" + codepoint.ToString("X4", CultureInfo.InvariantCulture)
|
||||
: "\\U" + codepoint.ToString("X8", CultureInfo.InvariantCulture);
|
||||
|
||||
private sealed class CodepointsFile(string path, string content) : IEquatable<CodepointsFile>
|
||||
{
|
||||
public string Path { get; } = path;
|
||||
|
||||
public string Content { get; } = content;
|
||||
|
||||
public bool Equals(CodepointsFile? other) => other is not null && this.Path == other.Path && this.Content == other.Content;
|
||||
|
||||
public override bool Equals(object? obj) => this.Equals(obj as CodepointsFile);
|
||||
|
||||
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(this.Path) ^ StringComparer.Ordinal.GetHashCode(this.Content);
|
||||
}
|
||||
|
||||
private sealed class IconDefinition(string name, int codepoint)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
|
||||
public int Codepoint { get; } = codepoint;
|
||||
|
||||
public string ConstantName { get; } = name.ToUpperInvariant().Replace('-', '_');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user