diff --git a/.editorconfig b/.editorconfig index a866b21..47022d2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,7 +17,7 @@ indent_size = 2 # C# and Visual Basic files [*.{cs,vb}] -charset = utf-8-bom +charset = utf-8 # Analyzers dotnet_analyzer_diagnostic.category-Security.severity = error diff --git a/BlazorSvgComponents.slnx b/BlazorSvgComponents.slnx index d433002..6aaf73a 100644 --- a/BlazorSvgComponents.slnx +++ b/BlazorSvgComponents.slnx @@ -1,4 +1,7 @@ + + + diff --git a/samples/HeatMap/Components/App.razor b/samples/HeatMap/Components/App.razor new file mode 100644 index 0000000..32bdc4a --- /dev/null +++ b/samples/HeatMap/Components/App.razor @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/HeatMap/Components/Layout/MainLayout.razor b/samples/HeatMap/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..96fbbe6 --- /dev/null +++ b/samples/HeatMap/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/samples/HeatMap/Components/Layout/MainLayout.razor.css b/samples/HeatMap/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..60cec92 --- /dev/null +++ b/samples/HeatMap/Components/Layout/MainLayout.razor.css @@ -0,0 +1,20 @@ +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + 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; + } diff --git a/samples/HeatMap/Components/Layout/ReconnectModal.razor b/samples/HeatMap/Components/Layout/ReconnectModal.razor new file mode 100644 index 0000000..49d916b --- /dev/null +++ b/samples/HeatMap/Components/Layout/ReconnectModal.razor @@ -0,0 +1,31 @@ + + + +
+ +

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +

+ The session has been paused by the server. +

+ +

+ Failed to resume the session.
Please reload the page. +

+
+
diff --git a/samples/HeatMap/Components/Layout/ReconnectModal.razor.css b/samples/HeatMap/Components/Layout/ReconnectModal.razor.css new file mode 100644 index 0000000..3ad3773 --- /dev/null +++ b/samples/HeatMap/Components/Layout/ReconnectModal.razor.css @@ -0,0 +1,157 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-pause-visible, +.components-resume-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-paused .components-pause-visible, +#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] + +{ + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; +} + +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/samples/HeatMap/Components/Layout/ReconnectModal.razor.js b/samples/HeatMap/Components/Layout/ReconnectModal.razor.js new file mode 100644 index 0000000..e52a190 --- /dev/null +++ b/samples/HeatMap/Components/Layout/ReconnectModal.razor.js @@ -0,0 +1,63 @@ +// Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + +const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); + +const resumeButton = document.getElementById("components-resume-button"); +resumeButton.addEventListener("click", resume); + +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } else if (event.detail.state === "rejected") { + location.reload(); + } +} + +async function retry() { + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + const resumeSuccessful = await Blazor.resumeCircuit(); + if (!resumeSuccessful) { + location.reload(); + } else { + reconnectModal.close(); + } + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} + +async function resume() { + try { + const successful = await Blazor.resumeCircuit(); + if (!successful) { + location.reload(); + } + } catch { + location.reload(); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === "visible") { + await retry(); + } +} diff --git a/samples/HeatMap/Components/Pages/Error.razor b/samples/HeatMap/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/samples/HeatMap/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/samples/HeatMap/Components/Pages/Home.razor b/samples/HeatMap/Components/Pages/Home.razor new file mode 100644 index 0000000..5551826 --- /dev/null +++ b/samples/HeatMap/Components/Pages/Home.razor @@ -0,0 +1,125 @@ +@page "/" +@using BlazorSvgComponents.Models +@using HotMap.Services +@using HotMap.Utils +@inject HeatMapService HeatMapInstance + +Heat Map! + + +
+

+ Heat map below: +

+
+ + + @foreach ((int i, string text) in _monthIndexes) + { + + } + + + @foreach ((string text, int i) in Weekdays.Select((s, i) => (s, i))) + { + + } + + + @foreach ((HeatMapGroupByWeek itemsByItem, int i) in _groupsByWeek.WithIndex()) + { + + @foreach ((HeatMapItem item, int j) in itemsByItem.Items.WithIndex()) + { + + } + + } + + +
+
+ +@code { + private const int Width = 10; + private const int Spacing = 2; + + private readonly record struct MonthIndex(int Pos, string Month); + + private readonly List _monthIndexes = []; + private readonly List _groupsByWeek = []; + + protected override void OnInitialized() + { + base.OnInitialized(); + + _groupsByWeek.AddRange(HeatMapInstance.GetItemsByWeek()); + + // To get the last item, we skip the first item. + // So index of current item is i + 1, and index of last item is i. + foreach ((HeatMapGroupByWeek group, int i) in _groupsByWeek.Skip(1).WithIndex()) + { + if (group.Monday.Month == _groupsByWeek[i].Monday.Month) + { + continue; + } + + // If current week item is not in the same month as the last week item. + _monthIndexes.Add(new MonthIndex(i + 1, Months[group.Monday.Month - 1])); + } + } + + private SvgViewBox GetHeatMapViewBox() + { + int width = 25 + Width * _groupsByWeek.Count + Spacing * (_groupsByWeek.Count - 1); + int height = 15 + Width * 7 + Spacing * 6; + + // Add an extra 10 pixels to make sure nothing is hidden. + return new SvgViewBox(0, 0, width + 10, height + 10); + } + + private static readonly List Months = + [ + "1月", "2月", "3月", "4月", "5月", "6月", + "7月", "8月", "9月", "10月", "11月", "12月" + ]; + + // private static readonly List Weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]; + private static readonly List Weekdays = ["周一", "周三", "周五"]; + + private static SvgTransform WeekdayGridTransform(int y) + { + return SvgTransform.CreateBuilder().Translate(0, y * (Width + Spacing)).Build(); + } + + private static SvgTransform WeekGridTransform(int x) + { + return SvgTransform.CreateBuilder().Translate(x * (Width + Spacing)).Build(); + } + + private static SvgTransform MonthTextTransform(int x) + { + return SvgTransform.CreateBuilder().Translate(x * (Width + Spacing)).Build(); + } + + private static SvgTransform DayTextTransform(int y) + { + // We only show Monday, Wednesday and Friday, so there are two days between texts. + return SvgTransform.CreateBuilder().Translate(0, y * 2 * (Width + Spacing)).Build(); + } + + private static string GetColorByContribution(int contribution) + { + return contribution switch + { + 0 => "fill-gray-200", + 1 or 2 => "fill-blue-100", + 3 or 4 => "fill-blue-300", + 5 or 6 => "fill-blue-500", + 7 or 8 => "fill-blue-700", + _ => "fill-blue-800" + }; + } + +} diff --git a/samples/HeatMap/Components/Pages/NotFound.razor b/samples/HeatMap/Components/Pages/NotFound.razor new file mode 100644 index 0000000..917ada1 --- /dev/null +++ b/samples/HeatMap/Components/Pages/NotFound.razor @@ -0,0 +1,5 @@ +@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/samples/HeatMap/Components/Routes.razor b/samples/HeatMap/Components/Routes.razor new file mode 100644 index 0000000..105855d --- /dev/null +++ b/samples/HeatMap/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/HeatMap/Components/_Imports.razor b/samples/HeatMap/Components/_Imports.razor new file mode 100644 index 0000000..7afa590 --- /dev/null +++ b/samples/HeatMap/Components/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using HotMap +@using HotMap.Components +@using HotMap.Components.Layout +@using BlazorSvgComponents diff --git a/samples/HeatMap/Directory.Build.targets b/samples/HeatMap/Directory.Build.targets new file mode 100644 index 0000000..fd87244 --- /dev/null +++ b/samples/HeatMap/Directory.Build.targets @@ -0,0 +1,53 @@ + + + pnpm install + pnpm run build + --output + + + + <_RestoreClientAssetsBeforeTargets Condition="'$(TargetFramework)' == ''">DispatchToInnerBuilds + + + + + + + + + + <_ClientAssetsOutputFullPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)ClientAssets')) + + + + + + + <_ClientAssetsBuildOutput Include="$(IntermediateOutputPath)ClientAssets\**"/> + + + + + + + + + + + + + + + + + + diff --git a/samples/HeatMap/HeatMap.csproj b/samples/HeatMap/HeatMap.csproj new file mode 100644 index 0000000..47d175f --- /dev/null +++ b/samples/HeatMap/HeatMap.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + true + HotMap + + + + + + + + pnpm install + pwsh build.ps1 + + diff --git a/samples/HeatMap/Program.cs b/samples/HeatMap/Program.cs new file mode 100644 index 0000000..622f748 --- /dev/null +++ b/samples/HeatMap/Program.cs @@ -0,0 +1,28 @@ +using HotMap.Components; +using HotMap.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} +app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/samples/HeatMap/Properties/launchSettings.json b/samples/HeatMap/Properties/launchSettings.json new file mode 100644 index 0000000..5f2d6d4 --- /dev/null +++ b/samples/HeatMap/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5184", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/samples/HeatMap/Services/HeatMapService.cs b/samples/HeatMap/Services/HeatMapService.cs new file mode 100644 index 0000000..eda9f7e --- /dev/null +++ b/samples/HeatMap/Services/HeatMapService.cs @@ -0,0 +1,1485 @@ +using System.Text.Json; +using HotMap.Utils; + +namespace HotMap.Services; + +public record HeatMapItem(DateOnly Time, int Contributions); + +public record HeatMapGroupByWeek(DateOnly Monday, List Items); + +public class HeatMapService(ILogger logger) +{ + private List? _cachedResult; + + public List GetItemsByWeek() + { + _cachedResult ??= CalculateByWeekItems(); + return _cachedResult; + } + + private record HeatMapItemInner(long Timestamp, int Contributions) + { + public DateTimeOffset Time => DateTimeOffset.FromUnixTimeSeconds(Timestamp); + } + + private List CalculateByWeekItems() + { + List? items = JsonSerializer.Deserialize>(Data, + s_jsonSerializerOptions); + + if (items is null or { Count: 0 }) + { + throw new InvalidOperationException("Failed to get heap map items."); + } + + // The contribution is not grouped by day, so group them. + IEnumerable groupedItems = items + .Select(i => (DateOnly.FromDateTime(i.Time.DateTime), i.Contributions)) + .GroupBy(pair => pair.Item1) + .Select(group => new HeatMapItem(group.Key, + group.Select(pair => pair.Contributions).Sum())); + + List result = new(52); + + // Consider the input data is in order. + // Start should be one year ago. + HeatMapGroupByWeek group = new(DateOnly.FromDateTime(DateTime.Now).AddDays(-365 - 7).GetMonday(), []); + logger.LogInformation("Create new item group by week {}.", group.Monday); + + foreach ((DateOnly date, int contributions) in groupedItems) + { + DateOnly mondayOfItem = date.GetMonday(); + logger.LogInformation("Current date of item: {}, monday is {}", date, mondayOfItem); + + // If current item is in the same week of last item. + if (mondayOfItem == group.Monday) + { + // Fill the spacing of empty days with 0 contribution. + FillSpacing(group, date); + + group.Items.Add(new HeatMapItem(date, contributions)); + continue; + } + + // Current time is in the next (or much more) week of last item. + // Fill the spacing, including the last week inner spacing and outer spacing. + while (group.Monday < mondayOfItem) + { + FillSpacing(group, date); + result.Add(group); + group = new HeatMapGroupByWeek(group.Monday.AddDays(7), []); + logger.LogInformation("Create new item group by week {}.", group.Monday); + } + + // Now, the inner spacing of one week. + FillSpacing(group, date); + group.Items.Add(new HeatMapItem(date, contributions)); + } + + // Not fill the last item and add directly. + result.Add(group); + + return result; + } + + private static void FillSpacing(HeatMapGroupByWeek groupByWeek, in DateOnly date) + { + if (groupByWeek.Monday == date) + { + return; + } + + if (groupByWeek.Items.Count == 0) + { + groupByWeek.Items.Add(new HeatMapItem(groupByWeek.Monday, 0)); + } + + DateOnly lastDate = groupByWeek.Items.Last().Time; + // The day in one week is 7, so th count of items of one week should not bigger than 7. + while (groupByWeek.Items.Count < 7 && lastDate < date.AddDays(-1)) + { + lastDate = lastDate.AddDays(1); + groupByWeek.Items.Add(new HeatMapItem(lastDate, 0)); + } + } + + private static readonly JsonSerializerOptions + s_jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; + + private const string Data = """ + [ + { + "timestamp": 1735529400, + "contributions": 1 + }, + { + "timestamp": 1735623900, + "contributions": 1 + }, + { + "timestamp": 1735717500, + "contributions": 1 + }, + { + "timestamp": 1735803000, + "contributions": 1 + }, + { + "timestamp": 1735827300, + "contributions": 1 + }, + { + "timestamp": 1735889400, + "contributions": 1 + }, + { + "timestamp": 1735976700, + "contributions": 1 + }, + { + "timestamp": 1736063100, + "contributions": 1 + }, + { + "timestamp": 1736135100, + "contributions": 1 + }, + { + "timestamp": 1736222400, + "contributions": 1 + }, + { + "timestamp": 1736316000, + "contributions": 1 + }, + { + "timestamp": 1736402400, + "contributions": 1 + }, + { + "timestamp": 1736492400, + "contributions": 1 + }, + { + "timestamp": 1736572500, + "contributions": 1 + }, + { + "timestamp": 1736658900, + "contributions": 1 + }, + { + "timestamp": 1736775900, + "contributions": 1 + }, + { + "timestamp": 1736830800, + "contributions": 1 + }, + { + "timestamp": 1736918100, + "contributions": 1 + }, + { + "timestamp": 1737018900, + "contributions": 1 + }, + { + "timestamp": 1737019800, + "contributions": 2 + }, + { + "timestamp": 1737036900, + "contributions": 1 + }, + { + "timestamp": 1737081900, + "contributions": 1 + }, + { + "timestamp": 1737186300, + "contributions": 1 + }, + { + "timestamp": 1737261000, + "contributions": 1 + }, + { + "timestamp": 1737342900, + "contributions": 1 + }, + { + "timestamp": 1737430200, + "contributions": 1 + }, + { + "timestamp": 1737516600, + "contributions": 1 + }, + { + "timestamp": 1737602100, + "contributions": 1 + }, + { + "timestamp": 1737696600, + "contributions": 1 + }, + { + "timestamp": 1737707400, + "contributions": 1 + }, + { + "timestamp": 1737708300, + "contributions": 7 + }, + { + "timestamp": 1737783900, + "contributions": 1 + }, + { + "timestamp": 1737786600, + "contributions": 1 + }, + { + "timestamp": 1737861300, + "contributions": 1 + }, + { + "timestamp": 1737947700, + "contributions": 1 + }, + { + "timestamp": 1737948600, + "contributions": 1 + }, + { + "timestamp": 1738035900, + "contributions": 1 + }, + { + "timestamp": 1738161900, + "contributions": 2 + }, + { + "timestamp": 1738219500, + "contributions": 1 + }, + { + "timestamp": 1738296000, + "contributions": 1 + }, + { + "timestamp": 1738380600, + "contributions": 1 + }, + { + "timestamp": 1738485000, + "contributions": 1 + }, + { + "timestamp": 1738552500, + "contributions": 1 + }, + { + "timestamp": 1738639800, + "contributions": 1 + }, + { + "timestamp": 1738725300, + "contributions": 1 + }, + { + "timestamp": 1738836900, + "contributions": 1 + }, + { + "timestamp": 1738899000, + "contributions": 1 + }, + { + "timestamp": 1738919700, + "contributions": 1 + }, + { + "timestamp": 1739160000, + "contributions": 1 + }, + { + "timestamp": 1739283300, + "contributions": 1 + }, + { + "timestamp": 1739333700, + "contributions": 1 + }, + { + "timestamp": 1739420100, + "contributions": 1 + }, + { + "timestamp": 1739523600, + "contributions": 1 + }, + { + "timestamp": 1739599200, + "contributions": 1 + }, + { + "timestamp": 1739678400, + "contributions": 1 + }, + { + "timestamp": 1739868300, + "contributions": 1 + }, + { + "timestamp": 1739943900, + "contributions": 1 + }, + { + "timestamp": 1739954700, + "contributions": 1 + }, + { + "timestamp": 1740021300, + "contributions": 1 + }, + { + "timestamp": 1740123000, + "contributions": 1 + }, + { + "timestamp": 1740195000, + "contributions": 1 + }, + { + "timestamp": 1740296700, + "contributions": 1 + }, + { + "timestamp": 1740367800, + "contributions": 1 + }, + { + "timestamp": 1740466800, + "contributions": 1 + }, + { + "timestamp": 1740540600, + "contributions": 1 + }, + { + "timestamp": 1740629700, + "contributions": 1 + }, + { + "timestamp": 1740636900, + "contributions": 1 + }, + { + "timestamp": 1740661200, + "contributions": 1 + }, + { + "timestamp": 1740662100, + "contributions": 5 + }, + { + "timestamp": 1740663900, + "contributions": 1 + }, + { + "timestamp": 1740714300, + "contributions": 1 + }, + { + "timestamp": 1740734100, + "contributions": 1 + }, + { + "timestamp": 1740735900, + "contributions": 1 + }, + { + "timestamp": 1740738600, + "contributions": 1 + }, + { + "timestamp": 1740741300, + "contributions": 1 + }, + { + "timestamp": 1740744000, + "contributions": 2 + }, + { + "timestamp": 1740799800, + "contributions": 1 + }, + { + "timestamp": 1740886200, + "contributions": 1 + }, + { + "timestamp": 1740981600, + "contributions": 1 + }, + { + "timestamp": 1741059900, + "contributions": 1 + }, + { + "timestamp": 1741077000, + "contributions": 1 + }, + { + "timestamp": 1741092300, + "contributions": 1 + }, + { + "timestamp": 1741093200, + "contributions": 1 + }, + { + "timestamp": 1741146300, + "contributions": 1 + }, + { + "timestamp": 1741155300, + "contributions": 1 + }, + { + "timestamp": 1741233600, + "contributions": 1 + }, + { + "timestamp": 1741320900, + "contributions": 1 + }, + { + "timestamp": 1741337100, + "contributions": 1 + }, + { + "timestamp": 1741349700, + "contributions": 2 + }, + { + "timestamp": 1741365000, + "contributions": 1 + }, + { + "timestamp": 1741413600, + "contributions": 1 + }, + { + "timestamp": 1741425300, + "contributions": 2 + }, + { + "timestamp": 1741442400, + "contributions": 1 + }, + { + "timestamp": 1741494600, + "contributions": 1 + }, + { + "timestamp": 1741580100, + "contributions": 1 + }, + { + "timestamp": 1741669200, + "contributions": 1 + }, + { + "timestamp": 1741758300, + "contributions": 1 + }, + { + "timestamp": 1741845600, + "contributions": 1 + }, + { + "timestamp": 1741881600, + "contributions": 1 + }, + { + "timestamp": 1741882500, + "contributions": 3 + }, + { + "timestamp": 1741883400, + "contributions": 1 + }, + { + "timestamp": 1741922100, + "contributions": 1 + }, + { + "timestamp": 1742013000, + "contributions": 1 + }, + { + "timestamp": 1742108400, + "contributions": 1 + }, + { + "timestamp": 1742184000, + "contributions": 1 + }, + { + "timestamp": 1742275800, + "contributions": 1 + }, + { + "timestamp": 1742358600, + "contributions": 1 + }, + { + "timestamp": 1742388300, + "contributions": 1 + }, + { + "timestamp": 1742392800, + "contributions": 1 + }, + { + "timestamp": 1742445000, + "contributions": 1 + }, + { + "timestamp": 1742457600, + "contributions": 1 + }, + { + "timestamp": 1742466600, + "contributions": 1 + }, + { + "timestamp": 1742469300, + "contributions": 1 + }, + { + "timestamp": 1742470200, + "contributions": 1 + }, + { + "timestamp": 1742474700, + "contributions": 1 + }, + { + "timestamp": 1742478300, + "contributions": 1 + }, + { + "timestamp": 1742481000, + "contributions": 1 + }, + { + "timestamp": 1742536800, + "contributions": 1 + }, + { + "timestamp": 1742544000, + "contributions": 1 + }, + { + "timestamp": 1742625000, + "contributions": 1 + }, + { + "timestamp": 1742635800, + "contributions": 1 + }, + { + "timestamp": 1742709600, + "contributions": 1 + }, + { + "timestamp": 1742792400, + "contributions": 1 + }, + { + "timestamp": 1742796000, + "contributions": 1 + }, + { + "timestamp": 1742826600, + "contributions": 2 + }, + { + "timestamp": 1742830200, + "contributions": 1 + }, + { + "timestamp": 1742832000, + "contributions": 1 + }, + { + "timestamp": 1742882400, + "contributions": 1 + }, + { + "timestamp": 1742885100, + "contributions": 1 + }, + { + "timestamp": 1742886000, + "contributions": 3 + }, + { + "timestamp": 1742886900, + "contributions": 1 + }, + { + "timestamp": 1742962500, + "contributions": 1 + }, + { + "timestamp": 1743056100, + "contributions": 1 + }, + { + "timestamp": 1743094800, + "contributions": 1 + }, + { + "timestamp": 1743135300, + "contributions": 1 + }, + { + "timestamp": 1743139800, + "contributions": 1 + }, + { + "timestamp": 1743227100, + "contributions": 1 + }, + { + "timestamp": 1743315300, + "contributions": 1 + }, + { + "timestamp": 1743390000, + "contributions": 1 + }, + { + "timestamp": 1743486300, + "contributions": 1 + }, + { + "timestamp": 1743581700, + "contributions": 1 + }, + { + "timestamp": 1743606900, + "contributions": 1 + }, + { + "timestamp": 1743660900, + "contributions": 1 + }, + { + "timestamp": 1743750000, + "contributions": 1 + }, + { + "timestamp": 1743836400, + "contributions": 1 + }, + { + "timestamp": 1743920100, + "contributions": 1 + }, + { + "timestamp": 1743997500, + "contributions": 1 + }, + { + "timestamp": 1744093800, + "contributions": 1 + }, + { + "timestamp": 1744268400, + "contributions": 1 + }, + { + "timestamp": 1744351200, + "contributions": 1 + }, + { + "timestamp": 1744443900, + "contributions": 1 + }, + { + "timestamp": 1744470000, + "contributions": 1 + }, + { + "timestamp": 1744524000, + "contributions": 1 + }, + { + "timestamp": 1744525800, + "contributions": 1 + }, + { + "timestamp": 1744601400, + "contributions": 2 + }, + { + "timestamp": 1744607700, + "contributions": 1 + }, + { + "timestamp": 1744638300, + "contributions": 5 + }, + { + "timestamp": 1744695000, + "contributions": 1 + }, + { + "timestamp": 1744773300, + "contributions": 1 + }, + { + "timestamp": 1744794000, + "contributions": 1 + }, + { + "timestamp": 1744872300, + "contributions": 1 + }, + { + "timestamp": 1744957800, + "contributions": 1 + }, + { + "timestamp": 1744959600, + "contributions": 1 + }, + { + "timestamp": 1745044200, + "contributions": 1 + }, + { + "timestamp": 1745046900, + "contributions": 1 + }, + { + "timestamp": 1745131500, + "contributions": 1 + }, + { + "timestamp": 1745214300, + "contributions": 1 + }, + { + "timestamp": 1745388000, + "contributions": 1 + }, + { + "timestamp": 1745474400, + "contributions": 2 + }, + { + "timestamp": 1745477100, + "contributions": 1 + }, + { + "timestamp": 1745487000, + "contributions": 1 + }, + { + "timestamp": 1745560800, + "contributions": 2 + }, + { + "timestamp": 1745584200, + "contributions": 1 + }, + { + "timestamp": 1745651700, + "contributions": 1 + }, + { + "timestamp": 1745724600, + "contributions": 1 + }, + { + "timestamp": 1745899200, + "contributions": 1 + }, + { + "timestamp": 1745903700, + "contributions": 4 + }, + { + "timestamp": 1745904600, + "contributions": 2 + }, + { + "timestamp": 1746083700, + "contributions": 1 + }, + { + "timestamp": 1746170100, + "contributions": 1 + }, + { + "timestamp": 1746252000, + "contributions": 1 + }, + { + "timestamp": 1746341100, + "contributions": 1 + }, + { + "timestamp": 1746446400, + "contributions": 1 + }, + { + "timestamp": 1746508500, + "contributions": 1 + }, + { + "timestamp": 1746590400, + "contributions": 1 + }, + { + "timestamp": 1746591300, + "contributions": 1 + }, + { + "timestamp": 1746675900, + "contributions": 1 + }, + { + "timestamp": 1746776700, + "contributions": 1 + }, + { + "timestamp": 1746810900, + "contributions": 1 + }, + { + "timestamp": 1746859500, + "contributions": 1 + }, + { + "timestamp": 1746949500, + "contributions": 1 + }, + { + "timestamp": 1747023300, + "contributions": 1 + }, + { + "timestamp": 1747123200, + "contributions": 1 + }, + { + "timestamp": 1747203300, + "contributions": 1 + }, + { + "timestamp": 1747290600, + "contributions": 1 + }, + { + "timestamp": 1747379700, + "contributions": 1 + }, + { + "timestamp": 1747461600, + "contributions": 1 + }, + { + "timestamp": 1747548000, + "contributions": 1 + }, + { + "timestamp": 1747620900, + "contributions": 1 + }, + { + "timestamp": 1747659600, + "contributions": 3 + }, + { + "timestamp": 1747660500, + "contributions": 1 + }, + { + "timestamp": 1747662300, + "contributions": 2 + }, + { + "timestamp": 1747663200, + "contributions": 3 + }, + { + "timestamp": 1747725300, + "contributions": 1 + }, + { + "timestamp": 1747796400, + "contributions": 1 + }, + { + "timestamp": 1747971900, + "contributions": 1 + }, + { + "timestamp": 1747973700, + "contributions": 1 + }, + { + "timestamp": 1748079000, + "contributions": 1 + }, + { + "timestamp": 1748163600, + "contributions": 1 + }, + { + "timestamp": 1748242800, + "contributions": 1 + }, + { + "timestamp": 1748323800, + "contributions": 1 + }, + { + "timestamp": 1748326500, + "contributions": 2 + }, + { + "timestamp": 1748412900, + "contributions": 1 + }, + { + "timestamp": 1748498400, + "contributions": 1 + }, + { + "timestamp": 1748504700, + "contributions": 3 + }, + { + "timestamp": 1748594700, + "contributions": 1 + }, + { + "timestamp": 1748762100, + "contributions": 1 + }, + { + "timestamp": 1748852100, + "contributions": 1 + }, + { + "timestamp": 1748855700, + "contributions": 1 + }, + { + "timestamp": 1748934900, + "contributions": 1 + }, + { + "timestamp": 1748937600, + "contributions": 1 + }, + { + "timestamp": 1749001500, + "contributions": 1 + }, + { + "timestamp": 1749114900, + "contributions": 1 + }, + { + "timestamp": 1749138300, + "contributions": 1 + }, + { + "timestamp": 1749142800, + "contributions": 2 + }, + { + "timestamp": 1749198600, + "contributions": 1 + }, + { + "timestamp": 1749279600, + "contributions": 1 + }, + { + "timestamp": 1749303900, + "contributions": 1 + }, + { + "timestamp": 1749367800, + "contributions": 1 + }, + { + "timestamp": 1749370500, + "contributions": 1 + }, + { + "timestamp": 1749443400, + "contributions": 1 + }, + { + "timestamp": 1749525300, + "contributions": 1 + }, + { + "timestamp": 1749737700, + "contributions": 3 + }, + { + "timestamp": 1749804300, + "contributions": 1 + }, + { + "timestamp": 1749901500, + "contributions": 1 + }, + { + "timestamp": 1749994200, + "contributions": 1 + }, + { + "timestamp": 1750003200, + "contributions": 2 + }, + { + "timestamp": 1750005900, + "contributions": 2 + }, + { + "timestamp": 1750055400, + "contributions": 1 + }, + { + "timestamp": 1750134600, + "contributions": 1 + }, + { + "timestamp": 1750226400, + "contributions": 1 + }, + { + "timestamp": 1750319100, + "contributions": 1 + }, + { + "timestamp": 1751040900, + "contributions": 1 + }, + { + "timestamp": 1751094900, + "contributions": 2 + }, + { + "timestamp": 1751103000, + "contributions": 1 + }, + { + "timestamp": 1751105700, + "contributions": 7 + }, + { + "timestamp": 1751462100, + "contributions": 1 + }, + { + "timestamp": 1753277400, + "contributions": 1 + }, + { + "timestamp": 1756614600, + "contributions": 2 + }, + { + "timestamp": 1756619100, + "contributions": 5 + }, + { + "timestamp": 1756818000, + "contributions": 1 + }, + { + "timestamp": 1760679900, + "contributions": 3 + }, + { + "timestamp": 1760704200, + "contributions": 1 + }, + { + "timestamp": 1760706000, + "contributions": 1 + }, + { + "timestamp": 1760706900, + "contributions": 1 + }, + { + "timestamp": 1760708700, + "contributions": 1 + }, + { + "timestamp": 1760775300, + "contributions": 1 + }, + { + "timestamp": 1760776200, + "contributions": 1 + }, + { + "timestamp": 1760778000, + "contributions": 1 + }, + { + "timestamp": 1760779800, + "contributions": 1 + }, + { + "timestamp": 1760782500, + "contributions": 1 + }, + { + "timestamp": 1760784300, + "contributions": 1 + }, + { + "timestamp": 1760862600, + "contributions": 1 + }, + { + "timestamp": 1760886000, + "contributions": 1 + }, + { + "timestamp": 1761126300, + "contributions": 3 + }, + { + "timestamp": 1761134400, + "contributions": 3 + }, + { + "timestamp": 1761137100, + "contributions": 1 + }, + { + "timestamp": 1761139800, + "contributions": 1 + }, + { + "timestamp": 1761747300, + "contributions": 1 + }, + { + "timestamp": 1761813900, + "contributions": 2 + }, + { + "timestamp": 1762354800, + "contributions": 1 + }, + { + "timestamp": 1762401600, + "contributions": 1 + }, + { + "timestamp": 1762416900, + "contributions": 1 + }, + { + "timestamp": 1762439400, + "contributions": 1 + }, + { + "timestamp": 1762441200, + "contributions": 5 + }, + { + "timestamp": 1762488000, + "contributions": 2 + }, + { + "timestamp": 1762509600, + "contributions": 3 + }, + { + "timestamp": 1762674300, + "contributions": 4 + }, + { + "timestamp": 1762764300, + "contributions": 2 + }, + { + "timestamp": 1763200800, + "contributions": 2 + }, + { + "timestamp": 1763201700, + "contributions": 4 + }, + { + "timestamp": 1763447400, + "contributions": 1 + }, + { + "timestamp": 1763564400, + "contributions": 2 + }, + { + "timestamp": 1763565300, + "contributions": 1 + }, + { + "timestamp": 1763627400, + "contributions": 2 + }, + { + "timestamp": 1763817300, + "contributions": 2 + }, + { + "timestamp": 1763818200, + "contributions": 4 + }, + { + "timestamp": 1764169200, + "contributions": 1 + }, + { + "timestamp": 1764170100, + "contributions": 4 + }, + { + "timestamp": 1764231300, + "contributions": 2 + }, + { + "timestamp": 1764257400, + "contributions": 1 + }, + { + "timestamp": 1764303300, + "contributions": 1 + }, + { + "timestamp": 1764306900, + "contributions": 1 + }, + { + "timestamp": 1764661500, + "contributions": 1 + }, + { + "timestamp": 1764662400, + "contributions": 5 + }, + { + "timestamp": 1764831600, + "contributions": 8 + }, + { + "timestamp": 1764836100, + "contributions": 1 + }, + { + "timestamp": 1764863100, + "contributions": 1 + }, + { + "timestamp": 1764908100, + "contributions": 1 + }, + { + "timestamp": 1764916200, + "contributions": 1 + }, + { + "timestamp": 1764938700, + "contributions": 1 + }, + { + "timestamp": 1765174500, + "contributions": 1 + }, + { + "timestamp": 1765184400, + "contributions": 1 + }, + { + "timestamp": 1765207800, + "contributions": 3 + }, + { + "timestamp": 1765208700, + "contributions": 2 + }, + { + "timestamp": 1765270800, + "contributions": 4 + }, + { + "timestamp": 1765271700, + "contributions": 3 + }, + { + "timestamp": 1765295100, + "contributions": 1 + }, + { + "timestamp": 1765370700, + "contributions": 7 + }, + { + "timestamp": 1765379700, + "contributions": 8 + }, + { + "timestamp": 1765426500, + "contributions": 2 + }, + { + "timestamp": 1765440900, + "contributions": 1 + }, + { + "timestamp": 1765444500, + "contributions": 4 + }, + { + "timestamp": 1765457100, + "contributions": 8 + }, + { + "timestamp": 1765640700, + "contributions": 2 + }, + { + "timestamp": 1765702800, + "contributions": 1 + }, + { + "timestamp": 1765703700, + "contributions": 4 + }, + { + "timestamp": 1765801800, + "contributions": 6 + }, + { + "timestamp": 1765877400, + "contributions": 2 + }, + { + "timestamp": 1765897200, + "contributions": 2 + }, + { + "timestamp": 1765898100, + "contributions": 1 + }, + { + "timestamp": 1765947600, + "contributions": 3 + }, + { + "timestamp": 1765983600, + "contributions": 2 + }, + { + "timestamp": 1765986300, + "contributions": 3 + }, + { + "timestamp": 1766031300, + "contributions": 1 + }, + { + "timestamp": 1766043000, + "contributions": 4 + }, + { + "timestamp": 1766043900, + "contributions": 1 + }, + { + "timestamp": 1766044800, + "contributions": 1 + }, + { + "timestamp": 1766045700, + "contributions": 1 + }, + { + "timestamp": 1766046600, + "contributions": 5 + }, + { + "timestamp": 1766241000, + "contributions": 1 + }, + { + "timestamp": 1766567700, + "contributions": 1 + }, + { + "timestamp": 1766652300, + "contributions": 1 + }, + { + "timestamp": 1767262500, + "contributions": 3 + }, + { + "timestamp": 1767359700, + "contributions": 1 + }, + { + "timestamp": 1767424500, + "contributions": 5 + }, + { + "timestamp": 1767426300, + "contributions": 1 + }, + { + "timestamp": 1767454200, + "contributions": 3 + }, + { + "timestamp": 1767456900, + "contributions": 1 + }, + { + "timestamp": 1767459600, + "contributions": 3 + }, + { + "timestamp": 1767518100, + "contributions": 1 + }, + { + "timestamp": 1767519900, + "contributions": 1 + }, + { + "timestamp": 1767627000, + "contributions": 5 + } + ] + """; +} diff --git a/samples/HeatMap/Utils/UtilsExtensions.cs b/samples/HeatMap/Utils/UtilsExtensions.cs new file mode 100644 index 0000000..e764701 --- /dev/null +++ b/samples/HeatMap/Utils/UtilsExtensions.cs @@ -0,0 +1,25 @@ +namespace HotMap.Utils; + +public static class UtilsExtensions +{ + extension(DateOnly date) + { + public DateOnly GetMonday() + { + return date.DayOfWeek switch + { + DayOfWeek.Monday => date, + DayOfWeek.Sunday => date.AddDays(-6), + _ => date.AddDays(1 - (int)date.DayOfWeek) + }; + } + } + + extension(IEnumerable enumerable) + { + public IEnumerable<(T, int)> WithIndex() + { + return enumerable.Select((v, i) => (v, i)); + } + } +} diff --git a/samples/HeatMap/appsettings.Development.json b/samples/HeatMap/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/HeatMap/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/HeatMap/appsettings.json b/samples/HeatMap/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/HeatMap/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/HeatMap/build.ps1 b/samples/HeatMap/build.ps1 new file mode 100644 index 0000000..6ef4bda --- /dev/null +++ b/samples/HeatMap/build.ps1 @@ -0,0 +1,11 @@ +[cmdletbinding()] +param( + [string]$output = "wwwroot" +) + + +$PSNativeCommandUseErrorActionPreference = $true +$ErrorActionPreference = "Stop" + +Write-Output "Output directory: $output" +pnpm tailwindcss -i wwwroot/tailwind.css -o $output/tailwind.g.css diff --git a/samples/HeatMap/package.json b/samples/HeatMap/package.json new file mode 100644 index 0000000..283f3e8 --- /dev/null +++ b/samples/HeatMap/package.json @@ -0,0 +1,17 @@ +{ + "name": "HotMap", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.26.2", + "dependencies": { + "@tailwindcss/cli": "^4.1.18", + "tailwindcss": "^4.1.18" + } +} diff --git a/samples/HeatMap/pnpm-lock.yaml b/samples/HeatMap/pnpm-lock.yaml new file mode 100644 index 0000000..38e0856 --- /dev/null +++ b/samples/HeatMap/pnpm-lock.yaml @@ -0,0 +1,622 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/cli': + specifier: ^4.1.18 + version: 4.1.18 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + +packages: + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@tailwindcss/cli@4.1.18': + resolution: {integrity: sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==} + hasBin: true + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + +snapshots: + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@tailwindcss/cli@4.1.18': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + enhanced-resolve: 5.18.4 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.1.18 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + graceful-fs@4.2.11: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + node-addon-api@7.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 diff --git a/samples/HeatMap/wwwroot/app.css b/samples/HeatMap/wwwroot/app.css new file mode 100644 index 0000000..5388357 --- /dev/null +++ b/samples/HeatMap/wwwroot/app.css @@ -0,0 +1,38 @@ +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.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." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} \ No newline at end of file diff --git a/samples/HeatMap/wwwroot/tailwind.css b/samples/HeatMap/wwwroot/tailwind.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/samples/HeatMap/wwwroot/tailwind.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/BlazorSvgComponents/BlazorSvgComponents.csproj b/src/BlazorSvgComponents/BlazorSvgComponents.csproj index 13ed70b..1b7f99c 100644 --- a/src/BlazorSvgComponents/BlazorSvgComponents.csproj +++ b/src/BlazorSvgComponents/BlazorSvgComponents.csproj @@ -6,7 +6,6 @@ enable - diff --git a/src/BlazorSvgComponents/Circle.razor b/src/BlazorSvgComponents/Circle.razor new file mode 100644 index 0000000..9448d26 --- /dev/null +++ b/src/BlazorSvgComponents/Circle.razor @@ -0,0 +1,11 @@ +@inherits SvgComponentBase + + + +@code { + [Parameter] public int CenterX { get; set; } + + [Parameter] public int CenterY { get; set; } + + [Parameter] public int Radius { get; set; } +} \ No newline at end of file diff --git a/src/BlazorSvgComponents/Ellipse.razor b/src/BlazorSvgComponents/Ellipse.razor new file mode 100644 index 0000000..63a489e --- /dev/null +++ b/src/BlazorSvgComponents/Ellipse.razor @@ -0,0 +1,13 @@ +@inherits SvgComponentBase + + + +@code { + [Parameter] public int CenterX { get; set; } + + [Parameter] public int CenterY { get; set; } + + [Parameter] public int EllipseX { get; set; } + + [Parameter] public int EllipseY { get; set; } +} diff --git a/src/BlazorSvgComponents/Line.razor b/src/BlazorSvgComponents/Line.razor new file mode 100644 index 0000000..6c99c01 --- /dev/null +++ b/src/BlazorSvgComponents/Line.razor @@ -0,0 +1,10 @@ +@using BlazorSvgComponents.Models +@inherits SvgComponentBase + + + +@code { + [Parameter] public SvgPoint Start { get; set; } + + [Parameter] public SvgPoint End { get; set; } +} diff --git a/src/BlazorSvgComponents/Models/SvgPoint.cs b/src/BlazorSvgComponents/Models/SvgPoint.cs new file mode 100644 index 0000000..94cb075 --- /dev/null +++ b/src/BlazorSvgComponents/Models/SvgPoint.cs @@ -0,0 +1,6 @@ +namespace BlazorSvgComponents.Models; + +public readonly record struct SvgPoint(int X, int Y) +{ + public override string ToString() => $"{X},{Y}"; +} diff --git a/src/BlazorSvgComponents/Models/SvgTransform.cs b/src/BlazorSvgComponents/Models/SvgTransform.cs new file mode 100644 index 0000000..863da4a --- /dev/null +++ b/src/BlazorSvgComponents/Models/SvgTransform.cs @@ -0,0 +1,73 @@ +using System.Text; + +namespace BlazorSvgComponents.Models; + +public readonly struct SvgTransform +{ + public string Transform { get; } + + internal SvgTransform(string transform) + { + Transform = transform; + } + + public override string ToString() => Transform; + + public static SvgTransformBuilder CreateBuilder() => new(); + + public static SvgTransform Empty => new(string.Empty); +} + +public class SvgTransformBuilder +{ + private readonly StringBuilder _builder = new(); + + public SvgTransformBuilder TransformMatrix(int a, int b, int c, int d, int e, int f) + { + _builder.Append("matrix(") + .Append(a).Append(',') + .Append(b).Append(',') + .Append(c).Append(',') + .Append(d).Append(',') + .Append(e).Append(',') + .Append(f).Append(')') + .Append(' '); + + return this; + } + + public SvgTransformBuilder Translate(int x, int y = 0) + { + _builder.Append($"translate({x} {y}) "); + return this; + } + + public SvgTransformBuilder Scale(int x, int y) + { + _builder.Append($"scale({x} {y}) "); + return this; + } + + public SvgTransformBuilder Rotate(int a, int x = 0, int y = 0) + { + _builder.Append($"rotate({a} {x} {y}) "); + return this; + } + + public SvgTransformBuilder SkewX(int a) + { + _builder.Append($"skewX({a}) "); + return this; + } + + public SvgTransformBuilder SkewY(int a) + { + _builder.Append($"skewY({a}) "); + return this; + } + + public SvgTransform Build() + { + return new SvgTransform(_builder.ToString().Trim()); + } +} diff --git a/src/BlazorSvgComponents/Models/SvgViewBox.cs b/src/BlazorSvgComponents/Models/SvgViewBox.cs new file mode 100644 index 0000000..100c5c0 --- /dev/null +++ b/src/BlazorSvgComponents/Models/SvgViewBox.cs @@ -0,0 +1,8 @@ +namespace BlazorSvgComponents.Models; + +public readonly record struct SvgViewBox(int StartX, int StartY, int EndX, int EndY) +{ + public override string ToString() => $"{StartX} {StartY} {EndX} {EndY}"; + + public bool IsEmpty => StartX == 0 && StartY == 0 && EndX == 0 && EndY == 0; +} diff --git a/src/BlazorSvgComponents/Polygon.razor b/src/BlazorSvgComponents/Polygon.razor new file mode 100644 index 0000000..473c946 --- /dev/null +++ b/src/BlazorSvgComponents/Polygon.razor @@ -0,0 +1,13 @@ +@using System.Text +@using BlazorSvgComponents.Models +@inherits SvgComponentBase + + + +@code { + [Parameter] public List Points { get; set; } = []; + + private string PointAttribute => + Points.Aggregate(new StringBuilder(), (builder, p) => builder.Append(' ').Append(p)).ToString().Trim(); + +} diff --git a/src/BlazorSvgComponents/Polyline.razor b/src/BlazorSvgComponents/Polyline.razor new file mode 100644 index 0000000..02316a1 --- /dev/null +++ b/src/BlazorSvgComponents/Polyline.razor @@ -0,0 +1,13 @@ +@using System.Text +@using BlazorSvgComponents.Models +@inherits SvgComponentBase + + + +@code { + [Parameter] public List Points { get; set; } = []; + + private string PointAttribute => + Points.Aggregate(new StringBuilder(), (builder, p) => builder.Append(' ').Append(p)).ToString().Trim(); + +} diff --git a/src/BlazorSvgComponents/Rectangle.razor b/src/BlazorSvgComponents/Rectangle.razor new file mode 100644 index 0000000..adc5ca8 --- /dev/null +++ b/src/BlazorSvgComponents/Rectangle.razor @@ -0,0 +1,17 @@ +@inherits SvgComponentBase + + + +@code { + [Parameter] public int X { get; set; } + + [Parameter] public int Y { get; set; } + + [Parameter] public int Width { get; set; } + + [Parameter] public int Height { get; set; } + + [Parameter] public int RadiusX { get; set; } + + [Parameter] public int RadiusY { get; set; } +} \ No newline at end of file diff --git a/src/BlazorSvgComponents/SvgComponentBase.cs b/src/BlazorSvgComponents/SvgComponentBase.cs new file mode 100644 index 0000000..b85e415 --- /dev/null +++ b/src/BlazorSvgComponents/SvgComponentBase.cs @@ -0,0 +1,68 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using Microsoft.AspNetCore.Components; +using BlazorSvgComponents.Models; + +namespace BlazorSvgComponents; + +public enum StrokeLineCapKind +{ + Butt, + Round, + Square +} + +public enum StrokeLineJoinKind +{ + Miter, + Round, + Bevel +} + +public abstract class SvgComponentBase : ComponentBase +{ + [Parameter] public string Fill { get; set; } = string.Empty; + + [Parameter] public string Stroke { get; set; } = string.Empty; + + [Parameter] public int StrokeWidth { get; set; } + + [Parameter] public float Opacity { get; set; } = 1f; + + [Parameter] public float FillOpacity { get; set; } = 1f; + + [Parameter] public float StrokeOpacity { get; set; } = 1f; + + [Parameter] public StrokeLineCapKind StrokeLineCap { get; set; } + + [Parameter] public StrokeLineJoinKind StrokeLineJoin { get; set; } + + [Parameter] public string Id { get; set; } = string.Empty; + + [Parameter] public string Class { get; set; } = string.Empty; + + [Parameter] public string Style { get; set; } = string.Empty; + + [Parameter] public string Title { get; set; } = string.Empty; + + [Parameter] public SvgTransform Transform { get; set; } = SvgTransform.Empty; + + protected IReadOnlyDictionary CommonAttributes => new ReadOnlyDictionary( + new Dictionary + { + { "fill", Fill }, + { "stroke", Stroke }, + { "stroke-width", StrokeWidth.ToString() }, + { "opacity", Opacity.ToString(CultureInfo.InvariantCulture) }, + { "fill-opacity", FillOpacity.ToString(CultureInfo.InvariantCulture) }, + { "stroke-opacity", StrokeOpacity.ToString(CultureInfo.InvariantCulture) }, + { "stroke-linecap", StrokeLineCap.ToString() }, + { "stroke-linejoin", StrokeLineJoin.ToString() }, + { "id", Id }, + { "class", Class }, + { "style", Style }, + { "title", Title }, + { "transform", Transform.ToString() }, + }.Where(pair => !string.IsNullOrEmpty(pair.Value)) + .Select(pair => new KeyValuePair(pair.Key, pair.Value)).ToDictionary()); +} diff --git a/src/BlazorSvgComponents/SvgContainer.razor b/src/BlazorSvgComponents/SvgContainer.razor new file mode 100644 index 0000000..00d013f --- /dev/null +++ b/src/BlazorSvgComponents/SvgContainer.razor @@ -0,0 +1,26 @@ +@using BlazorSvgComponents.Models +@if (ViewBox.IsEmpty) +{ + + @ChildContent + +} +else +{ + + @ChildContent + +} + + +@code +{ + [Parameter] public RenderFragment? ChildContent { get; set; } + + [Parameter] public string Width { get; set; } = "300"; + + + [Parameter] public string Height { get; set; } = "150"; + + [Parameter] public SvgViewBox ViewBox { get; set; } +} diff --git a/src/BlazorSvgComponents/SvgGroup.razor b/src/BlazorSvgComponents/SvgGroup.razor new file mode 100644 index 0000000..83d1471 --- /dev/null +++ b/src/BlazorSvgComponents/SvgGroup.razor @@ -0,0 +1,7 @@ +@inherits SvgComponentBase + +@ChildContent + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } +} \ No newline at end of file diff --git a/src/BlazorSvgComponents/SvgText.razor b/src/BlazorSvgComponents/SvgText.razor new file mode 100644 index 0000000..cc13b8b --- /dev/null +++ b/src/BlazorSvgComponents/SvgText.razor @@ -0,0 +1,14 @@ +@inherits SvgComponentBase + + + @Content + + +@code { + [Parameter] public string Content { get; set; } = string.Empty; + + [Parameter] public int X { get; set; } + + [Parameter] public int Y { get; set; } + +} \ No newline at end of file