added aquarium

This commit is contained in:
Michal Vrba
2026-09-29 14:25:04 +02:00
parent da4df83e13
commit a5410bb5d7
80 changed files with 62854 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.12" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aquarium.Shared\Aquarium.Shared.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,5 @@
@inherits LayoutComponentBase
<div class="aquarium-app-root">
@Body
</div>
@@ -0,0 +1,77 @@
.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 {
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;
}
}
+39
View File
@@ -0,0 +1,39 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">VrbaOne.Aquarium.Client</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
@@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
+243
View File
@@ -0,0 +1,243 @@
@page "/"
@inject AquariumClientService Aquarium
@inject IJSRuntime JS
@implements IDisposable
<div class="aquarium-container">
<!-- HTML5 60 FPS Marine Canvas -->
<canvas id="aquariumCanvas"></canvas>
<!-- Top Floating Cyber-Metrics Bar -->
<header class="top-metrics-bar glass-panel">
<div class="brand-section">
<span class="brand-logo">🌊</span>
<div>
<h1 class="brand-title">NETWORK AQUARIUM</h1>
<p class="brand-subtitle">Realtime Server Traffic Visualizer &middot; .NET 10 WASM</p>
</div>
</div>
<div class="kpi-container">
<div class="kpi-item">
<span class="kpi-label">Throughput</span>
<span class="kpi-value cyan">@Aquarium.CurrentStats.RequestsPerSecond <small style="font-size:0.65rem;">req/s</small></span>
</div>
<div class="kpi-item">
<span class="kpi-label">Avg Latency</span>
<span class="kpi-value @(Aquarium.CurrentStats.AverageLatencyMs < 100 ? "emerald" : "amber")">@Aquarium.CurrentStats.AverageLatencyMs <small style="font-size:0.65rem;">ms</small></span>
</div>
<div class="kpi-item">
<span class="kpi-label">Error Rate</span>
<span class="kpi-value @(Aquarium.CurrentStats.ErrorRatePercent == 0 ? "emerald" : "rose")">@Aquarium.CurrentStats.ErrorRatePercent%</span>
</div>
<div class="kpi-item">
<span class="kpi-label">Total Data</span>
<span class="kpi-value">@FormatBytes(Aquarium.CurrentStats.TotalBytesTransferred)</span>
</div>
<div class="status-badge @(Aquarium.IsUsingLocalSimulation ? "reconnecting" : "online")">
<span class="pulsing-dot"></span>
<span>@(Aquarium.IsUsingLocalSimulation ? "DEMO SIMULATION" : "LIVE SERVER")</span>
</div>
</div>
</header>
<!-- Floating Left Filter & Biomes Panel -->
<aside class="left-controls glass-panel">
<div class="control-title">🤿 Marine Biomes</div>
<div class="biome-btn-group">
<button class="btn-biome @(_selectedBiome == "ALL" ? "active" : "")" @onclick='() => SetBiomeFilter("ALL")'>
🌐 All Depths
</button>
<button class="btn-biome @(_selectedBiome == "Surface" ? "active" : "")" @onclick='() => SetBiomeFilter("Surface")'>
☀️ Surface (CDN & WS)
</button>
<button class="btn-biome @(_selectedBiome == "Pelagic" ? "active" : "")" @onclick='() => SetBiomeFilter("Pelagic")'>
🐠 Pelagic (REST API)
</button>
<button class="btn-biome @(_selectedBiome == "Abyssal" ? "active" : "")" @onclick='() => SetBiomeFilter("Abyssal")'>
🪸 Abyssal (DB & Jobs)
</button>
</div>
<div class="control-title" style="margin-top: 12px;">🔍 Filter Traffic</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search path / regex..." value="@_searchQuery" @oninput="OnSearchChanged" />
</div>
<div style="display: flex; gap: 8px; margin-top: 10px;">
<button class="btn-action" style="flex: 1; justify-content: center;" @onclick="ToggleAudio">
@(Aquarium.SoundEnabled ? "🔊 Sound ON" : "🔇 Sound OFF")
</button>
<button class="btn-action" style="justify-content: center;" title="Clear screen" @onclick="ClearCreatures">
🧹
</button>
</div>
</aside>
<!-- Bottom Simulation & Trigger Toolbar -->
<div class="bottom-toolbar glass-panel">
<span style="font-size: 0.75rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-right: 4px;">Inject Traffic:</span>
<button class="btn-action" @onclick="() => Spawn(CreatureType.Fish)">🐟 HTTP Fish</button>
<button class="btn-action dolphin" @onclick="() => Spawn(CreatureType.Dolphin)">🐬 WebSocket</button>
<button class="btn-action whale" @onclick="() => Spawn(CreatureType.Whale)">🐋 >5MB Whale</button>
<button class="btn-action jelly" @onclick="() => Spawn(CreatureType.Jellyfish)">🪼 500 Error Jelly</button>
<button class="btn-action shark" @onclick="() => Spawn(CreatureType.Shark)">🦈 429 Shark</button>
<button class="btn-action crab" @onclick="() => Spawn(CreatureType.Crab)">🦀 DB Crab</button>
</div>
<!-- Marine Legend at Bottom Left -->
<div class="legend-panel glass-panel">
<div class="legend-item"><span>🐟</span> <span>GET/POST</span></div>
<div class="legend-item"><span>🐬</span> <span>WebSockets</span></div>
<div class="legend-item"><span>🐋</span> <span>Payload &gt;1MB</span></div>
<div class="legend-item"><span>🪼</span> <span>4xx/5xx Errors</span></div>
<div class="legend-item"><span>🦈</span> <span>Rate-Limit 429</span></div>
<div class="legend-item"><span>🦀</span> <span>DB / Cron</span></div>
</div>
<!-- Slide-over Telemetry Inspector Drawer -->
<section class="inspector-drawer glass-panel @(Aquarium.SelectedEvent == null ? "closed" : "")">
@if (Aquarium.SelectedEvent != null)
{
var e = Aquarium.SelectedEvent;
<div class="inspector-header">
<div class="inspector-title">📡 TELEMETRY INSPECT</div>
<button class="btn-close" @onclick="CloseDrawer">&times;</button>
</div>
<!-- Route & Status Card -->
<div class="inspect-card">
<div class="card-row">
<span class="http-badge @e.Method">@e.Method</span>
<span class="status-tag @(e.StatusCode >= 500 ? "s5xx" : e.StatusCode >= 400 ? "s4xx" : "s2xx")">
@e.StatusCode @(e.StatusCode == 200 ? "OK" : e.StatusCode == 101 ? "Switching Protocols" : e.StatusCode == 404 ? "Not Found" : e.StatusCode == 500 ? "Internal Error" : "")
</span>
</div>
<div class="path-box">@e.Path</div>
<div class="card-row">
<span class="card-label">Timestamp</span>
<span class="card-value">@e.Timestamp.ToString("HH:mm:ss.fff")</span>
</div>
</div>
<!-- Performance Card -->
<div class="inspect-card">
<div class="card-row">
<span class="card-label">⏱ Response Time (Latency)</span>
<span class="card-value" style="color: #38bdf8;">@e.DurationMs.ToString("F1") ms</span>
</div>
<div class="card-row">
<span class="card-label">📦 Transferred Size</span>
<span class="card-value">@FormatBytes(e.Bytes)</span>
</div>
@if (e.DatabaseTimeMs.HasValue)
{
<div class="card-row">
<span class="card-label">🗄 DB Execution Time</span>
<span class="card-value" style="color: #f97316;">@e.DatabaseTimeMs.Value.ToString("F1") ms (@e.QueryCount queries)</span>
</div>
}
</div>
<!-- Client Info Card -->
<div class="inspect-card">
<div class="card-row">
<span class="card-label">🌍 Client Origin</span>
<span class="card-value">@e.ClientCity, @e.ClientCountry</span>
</div>
<div class="card-row">
<span class="card-label">🖥 Client IP</span>
<span class="card-value">@e.ClientIp</span>
</div>
<div class="card-row">
<span class="card-label">🛡 Service Target</span>
<span class="card-value">@e.ServiceName</span>
</div>
</div>
<!-- Diagnostic Trace ID -->
<div class="inspect-card">
<div class="card-row">
<span class="card-label">🔗 Trace Identifier</span>
</div>
<div class="card-row" style="margin-top: 4px;">
<span class="card-value" style="font-size: 0.72rem; color: #a5b4fc; word-break: break-all;">@e.TraceId</span>
</div>
@if (!string.IsNullOrWhiteSpace(e.ErrorMessage))
{
<div style="margin-top: 10px; padding: 8px; background: rgba(239, 68, 68, 0.15); border-left: 3px solid #ef4444; border-radius: 4px; font-size: 0.72rem; color: #fca5a5; font-family: 'JetBrains Mono', monospace;">
@e.ErrorMessage
</div>
}
</div>
}
</section>
</div>
@code {
private string _selectedBiome = "ALL";
private string _searchQuery = "";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Aquarium.OnStateChanged += StateHasChanged;
await Aquarium.InitializeAsync("aquariumCanvas");
}
}
private async Task Spawn(CreatureType type)
{
await Aquarium.TriggerCreatureAsync(type);
}
private async Task ToggleAudio()
{
await Aquarium.ToggleSoundAsync();
}
private async Task ClearCreatures()
{
await Aquarium.ClearAllCreaturesAsync();
}
private async Task SetBiomeFilter(string biome)
{
_selectedBiome = biome;
await Aquarium.SetFiltersAsync("ALL", _selectedBiome, _searchQuery);
}
private async Task OnSearchChanged(ChangeEventArgs e)
{
_searchQuery = e.Value?.ToString() ?? "";
await Aquarium.SetFiltersAsync("ALL", _selectedBiome, _searchQuery);
}
private void CloseDrawer()
{
Aquarium.CloseInspector();
}
private string FormatBytes(long bytes)
{
string[] sizes = ["B", "KB", "MB", "GB"];
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len /= 1024;
}
return $"{len:0.#} {sizes[order]}";
}
public void Dispose()
{
Aquarium.OnStateChanged -= StateHasChanged;
}
}
+5
View File
@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
+13
View File
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Aquarium.Client;
using Aquarium.Client.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<AquariumClientService>();
await builder.Build().RunAsync();
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7100;http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,298 @@
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.JSInterop;
using Aquarium.Shared;
namespace Aquarium.Client.Services;
public class AquariumClientService : IAsyncDisposable
{
private readonly IJSRuntime _js;
private HubConnection? _hubConnection;
private DotNetObjectReference<AquariumClientService>? _dotNetRef;
private CancellationTokenSource? _localSimulationCts;
private readonly Random _rnd = new();
public string ServerUrl { get; set; } = "http://localhost:5050/hubs/aquarium";
public HubConnectionState ConnectionState => _hubConnection?.State ?? HubConnectionState.Disconnected;
public bool IsUsingLocalSimulation { get; private set; } = false;
public bool SoundEnabled { get; set; } = false;
public AquariumStats CurrentStats { get; private set; } = new()
{
RequestsPerSecond = 0,
AverageLatencyMs = 28,
ErrorRatePercent = 0.0,
TotalRequests = 0,
TotalBytesTransferred = 0
};
public TrafficEvent? SelectedEvent { get; private set; }
public event Action? OnStateChanged;
public event Action<TrafficEvent>? OnCreatureClicked;
public AquariumClientService(IJSRuntime js)
{
_js = js;
}
public async Task InitializeAsync(string canvasId)
{
_dotNetRef = DotNetObjectReference.Create(this);
await _js.InvokeVoidAsync("AquariumEngine.init", canvasId, _dotNetRef);
await ConnectToServerAsync();
}
public async Task ConnectToServerAsync()
{
try
{
if (_hubConnection != null)
{
await _hubConnection.DisposeAsync();
}
_hubConnection = new HubConnectionBuilder()
.WithUrl(ServerUrl)
.WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) })
.Build();
_hubConnection.On<CreatureSpawnDto>("OnCreatureSpawn", async creature =>
{
await _js.InvokeVoidAsync("AquariumEngine.spawnCreature", creature);
});
_hubConnection.On<List<CreatureSpawnDto>>("OnBatchSpawn", async batch =>
{
await _js.InvokeVoidAsync("AquariumEngine.spawnBatch", batch);
});
_hubConnection.On<AquariumStats>("OnStatsUpdated", stats =>
{
CurrentStats = stats;
OnStateChanged?.Invoke();
});
_hubConnection.Reconnecting += ex =>
{
OnStateChanged?.Invoke();
return Task.CompletedTask;
};
_hubConnection.Reconnected += id =>
{
IsUsingLocalSimulation = false;
StopLocalSimulation();
OnStateChanged?.Invoke();
return Task.CompletedTask;
};
_hubConnection.Closed += ex =>
{
StartLocalSimulationFallback();
OnStateChanged?.Invoke();
return Task.CompletedTask;
};
await _hubConnection.StartAsync();
IsUsingLocalSimulation = false;
StopLocalSimulation();
OnStateChanged?.Invoke();
}
catch (Exception)
{
StartLocalSimulationFallback();
OnStateChanged?.Invoke();
}
}
public async Task ToggleSoundAsync()
{
SoundEnabled = await _js.InvokeAsync<bool>("AquariumEngine.toggleSound");
OnStateChanged?.Invoke();
}
public async Task SetFiltersAsync(string method, string biome, string query)
{
await _js.InvokeVoidAsync("AquariumEngine.setFilters", method, biome, query);
}
public async Task ClearAllCreaturesAsync()
{
await _js.InvokeVoidAsync("AquariumEngine.clearAll");
SelectedEvent = null;
OnStateChanged?.Invoke();
}
public async Task TriggerCreatureAsync(CreatureType type)
{
var evt = GenerateMockEvent(type);
var spawnDto = CreatureSpawnDto.FromEvent(evt);
await _js.InvokeVoidAsync("AquariumEngine.spawnCreature", spawnDto);
CurrentStats.TotalRequests++;
CurrentStats.TotalBytesTransferred += evt.Bytes;
if (evt.StatusCode >= 500) CurrentStats.Total5xx++;
else if (evt.StatusCode >= 400) CurrentStats.Total4xx++;
else CurrentStats.Total2xx++;
OnStateChanged?.Invoke();
}
[JSInvokable]
public void OnCreatureSelected(TrafficEvent selected)
{
SelectedEvent = selected;
OnCreatureClicked?.Invoke(selected);
OnStateChanged?.Invoke();
}
public void CloseInspector()
{
SelectedEvent = null;
OnStateChanged?.Invoke();
}
private void StartLocalSimulationFallback()
{
if (IsUsingLocalSimulation) return;
IsUsingLocalSimulation = true;
_localSimulationCts?.Cancel();
_localSimulationCts = new CancellationTokenSource();
var token = _localSimulationCts.Token;
_ = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
try
{
var roll = _rnd.Next(100);
CreatureType type;
if (roll < 5) type = CreatureType.Jellyfish;
else if (roll < 10) type = CreatureType.Whale;
else if (roll < 15) type = CreatureType.Dolphin;
else if (roll < 19) type = CreatureType.Crab;
else if (roll < 22) type = CreatureType.Shark;
else type = CreatureType.Fish;
var evt = GenerateMockEvent(type);
var spawn = CreatureSpawnDto.FromEvent(evt);
await _js.InvokeVoidAsync("AquariumEngine.spawnCreature", spawn);
CurrentStats.TotalRequests++;
CurrentStats.TotalBytesTransferred += evt.Bytes;
CurrentStats.RequestsPerSecond = Math.Round(2.0 + _rnd.NextDouble() * 4.0, 1);
CurrentStats.AverageLatencyMs = Math.Round(35.0 + _rnd.NextDouble() * 30.0, 1);
var delay = _rnd.Next(400, 1400);
await Task.Delay(delay, token);
}
catch (OperationCanceledException) { break; }
catch { await Task.Delay(1000, token); }
}
}, token);
}
private void StopLocalSimulation()
{
_localSimulationCts?.Cancel();
_localSimulationCts = null;
}
private TrafficEvent GenerateMockEvent(CreatureType type)
{
return type switch
{
CreatureType.Whale => new TrafficEvent
{
Method = "GET",
Path = "/download/release-v2.iso",
StatusCode = 200,
DurationMs = 1200,
Bytes = 8_400_000,
ClientCountry = "DE",
ClientCity = "Frankfurt",
ClientIp = "194.25.0.12",
TraceId = $"whl_{Guid.NewGuid():N}"[..12]
},
CreatureType.Dolphin => new TrafficEvent
{
Method = "WS",
Path = "/signalr/traffic-feed",
StatusCode = 101,
DurationMs = 8,
Bytes = 1024,
IsWebSocket = true,
ClientCountry = "SK",
ClientCity = "Bratislava",
ClientIp = "178.40.12.8",
TraceId = $"ws_{Guid.NewGuid():N}"[..12]
},
CreatureType.Jellyfish => new TrafficEvent
{
Method = "POST",
Path = "/api/v1/auth/session/renew",
StatusCode = _rnd.Next(0, 2) == 0 ? 500 : 404,
DurationMs = 340,
Bytes = 512,
ClientCountry = "CZ",
ClientCity = "Brno",
ClientIp = "89.24.18.99",
TraceId = $"err_{Guid.NewGuid():N}"[..12],
ErrorMessage = "System.TimeoutException: Database pool exhausted"
},
CreatureType.Shark => new TrafficEvent
{
Method = "GET",
Path = "/api/v1/search?term=ddos_burst",
StatusCode = 429,
DurationMs = 15,
Bytes = 256,
ClientCountry = "US",
ClientCity = "Ashburn",
ClientIp = "34.201.88.19",
TraceId = $"shk_{Guid.NewGuid():N}"[..12],
ErrorMessage = "RateLimit: 429 Too Many Requests"
},
CreatureType.Crab => new TrafficEvent
{
Method = "JOB",
Path = "/cron/db-cache-prune",
StatusCode = 200,
DurationMs = 450,
Bytes = 4096,
ClientCountry = "SK",
ClientCity = "Bratislava",
ClientIp = "127.0.0.1",
TraceId = $"job_{Guid.NewGuid():N}"[..12],
DatabaseTimeMs = 38.0,
QueryCount = 8
},
_ => new TrafficEvent
{
Method = new[] { "GET", "POST", "PUT", "DELETE" }[_rnd.Next(4)],
Path = new[] { "/api/v1/users", "/api/v1/stats", "/api/v1/orders", "/api/v1/feed" }[_rnd.Next(4)],
StatusCode = 200,
DurationMs = _rnd.Next(15, 95),
Bytes = _rnd.Next(1024, 25000),
ClientCountry = "SK",
ClientCity = "Bratislava",
ClientIp = $"188.167.{_rnd.Next(1, 250)}.{_rnd.Next(1, 250)}",
TraceId = $"trc_{Guid.NewGuid():N}"[..12]
}
};
}
public async ValueTask DisposeAsync()
{
StopLocalSimulation();
if (_hubConnection != null)
{
await _hubConnection.DisposeAsync();
}
_dotNetRef?.Dispose();
}
}
+12
View File
@@ -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 Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using Aquarium.Client
@using Aquarium.Client.Layout
@using Aquarium.Client.Services
@using Aquarium.Shared
+439
View File
@@ -0,0 +1,439 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Orbitron:wght@600;800;900&display=swap');
:root {
--bg-ocean-dark: #000814;
--bg-panel: rgba(10, 25, 47, 0.75);
--bg-panel-border: rgba(56, 189, 248, 0.2);
--accent-cyan: #06b6d4;
--accent-blue: #38bdf8;
--accent-emerald: #10b981;
--accent-amber: #f59e0b;
--accent-rose: #f43f5e;
--accent-purple: #a855f7;
--text-main: #f8fafc;
--text-muted: #94a3b8;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body, html {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: var(--bg-ocean-dark);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--text-main);
}
.aquarium-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#aquariumCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
z-index: 1;
}
/* Glassmorphism utility */
.glass-panel {
background: var(--bg-panel);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--bg-panel-border);
border-radius: 12px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.45);
}
/* Top Metrics Bar */
.top-metrics-bar {
position: absolute;
top: 16px;
left: 16px;
right: 16px;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
pointer-events: auto;
}
.brand-section {
display: flex;
align-items: center;
gap: 12px;
}
.brand-logo {
font-size: 24px;
animation: waveBounce 3s ease-in-out infinite;
}
@keyframes waveBounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.brand-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.1rem;
font-weight: 800;
letter-spacing: 1.5px;
background: linear-gradient(135deg, #38bdf8 0%, #06b6d4 50%, #10b981 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.brand-subtitle {
font-size: 0.72rem;
font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
}
.kpi-container {
display: flex;
align-items: center;
gap: 24px;
}
.kpi-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.kpi-label {
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
}
.kpi-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.15rem;
font-weight: 700;
color: var(--text-main);
}
.kpi-value.cyan { color: var(--accent-cyan); }
.kpi-value.emerald { color: var(--accent-emerald); }
.kpi-value.rose { color: var(--accent-rose); }
.kpi-value.amber { color: var(--accent-amber); }
.status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}
.status-badge.online {
background: rgba(16, 185, 129, 0.15);
border: 1px solid rgba(16, 185, 129, 0.4);
color: #34d399;
}
.status-badge.reconnecting {
background: rgba(245, 158, 11, 0.15);
border: 1px solid rgba(245, 158, 11, 0.4);
color: #fbbf24;
}
.status-badge.offline {
background: rgba(244, 63, 94, 0.15);
border: 1px solid rgba(244, 63, 94, 0.4);
color: #fb7185;
}
.pulsing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: currentColor;
animation: pulseDot 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulseDot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.85); }
}
/* Floating Left Controls / Biomes */
.left-controls {
position: absolute;
top: 90px;
left: 16px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 12px;
width: 220px;
padding: 16px;
}
.control-title {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
margin-bottom: 6px;
}
.biome-btn-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.btn-biome {
display: flex;
align-items: center;
gap: 8px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
color: var(--text-muted);
padding: 8px 12px;
border-radius: 8px;
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-biome:hover {
background: rgba(56, 189, 248, 0.15);
border-color: rgba(56, 189, 248, 0.3);
color: var(--text-main);
}
.btn-biome.active {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.3) 0%, rgba(56, 189, 248, 0.2) 100%);
border-color: var(--accent-blue);
color: #fff;
box-shadow: 0 0 12px rgba(56, 189, 248, 0.2);
}
/* Search input */
.search-box {
position: relative;
width: 100%;
}
.search-input {
width: 100%;
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px 12px;
font-size: 0.78rem;
font-family: 'JetBrains Mono', monospace;
color: #fff;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: var(--accent-blue);
}
/* Bottom Simulation & Trigger Toolbar */
.bottom-toolbar {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 18px;
}
.btn-action {
display: flex;
align-items: center;
gap: 6px;
background: rgba(15, 23, 42, 0.75);
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text-main);
padding: 8px 14px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-action:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.btn-action.whale:hover { border-color: #3b82f6; background: rgba(59, 130, 246, 0.2); }
.btn-action.dolphin:hover { border-color: #06b6d4; background: rgba(6, 182, 212, 0.2); }
.btn-action.jelly:hover { border-color: #ef4444; background: rgba(239, 68, 68, 0.2); }
.btn-action.shark:hover { border-color: #e11d48; background: rgba(225, 29, 72, 0.2); }
.btn-action.crab:hover { border-color: #f97316; background: rgba(249, 115, 22, 0.2); }
/* Slide-in Telemetry Inspector Drawer */
.inspector-drawer {
position: absolute;
top: 90px;
right: 16px;
bottom: 16px;
width: 360px;
z-index: 20;
display: flex;
flex-direction: column;
padding: 20px;
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
transform: translateX(0);
opacity: 1;
overflow-y: auto;
}
.inspector-drawer.closed {
transform: translateX(400px);
opacity: 0;
pointer-events: none;
}
.inspector-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 16px;
}
.inspector-title {
font-family: 'Orbitron', sans-serif;
font-size: 0.95rem;
font-weight: 700;
color: var(--accent-blue);
}
.btn-close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.2rem;
cursor: pointer;
line-height: 1;
}
.btn-close:hover { color: #fff; }
.inspect-card {
background: rgba(15, 23, 42, 0.55);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 0.8rem;
}
.card-row:last-child { margin-bottom: 0; }
.card-label { color: var(--text-muted); }
.card-value {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
color: var(--text-main);
}
.http-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.72rem;
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
}
.http-badge.GET { background: rgba(6, 182, 212, 0.2); color: #22d3ee; border: 1px solid #0891b2; }
.http-badge.POST { background: rgba(16, 185, 129, 0.2); color: #34d399; border: 1px solid #059669; }
.http-badge.PUT { background: rgba(139, 92, 246, 0.2); color: #a78bfa; border: 1px solid #7c3aed; }
.http-badge.DELETE { background: rgba(244, 63, 94, 0.2); color: #fb7185; border: 1px solid #e11d48; }
.http-badge.WS { background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid #0284c7; }
.http-badge.JOB { background: rgba(249, 115, 22, 0.2); color: #fb923c; border: 1px solid #ea580c; }
.status-tag {
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.72rem;
font-weight: 700;
}
.status-tag.s2xx { background: rgba(16, 185, 129, 0.2); color: #34d399; }
.status-tag.s4xx { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
.status-tag.s5xx { background: rgba(239, 68, 68, 0.25); color: #f87171; border: 1px solid #ef4444; }
.path-box {
background: rgba(0, 0, 0, 0.4);
border-radius: 6px;
padding: 8px 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
color: #38bdf8;
word-break: break-all;
margin: 8px 0;
}
.copy-btn {
background: rgba(255, 255, 255, 0.08);
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 0.7rem;
color: var(--text-muted);
cursor: pointer;
}
.copy-btn:hover { background: rgba(255, 255, 255, 0.15); color: #fff; }
/* Marine Legend at Bottom Left */
.legend-panel {
position: absolute;
bottom: 16px;
left: 16px;
z-index: 10;
padding: 10px 14px;
display: flex;
align-items: center;
gap: 14px;
font-size: 0.72rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>🌊 Network Aquarium - Realtime Server Traffic Visualizer</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
</head>
<body>
<div id="app">
<div style="position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #000814; color: #38bdf8; font-family: sans-serif;">
<div style="font-size: 3rem; margin-bottom: 1rem; animation: pulse 1.5s infinite;">🌊</div>
<h2 style="font-family: 'Orbitron', monospace; font-size: 1.2rem; letter-spacing: 2px;">DIVING INTO NETWORK AQUARIUM...</h2>
<p style="color: #64748b; font-size: 0.85rem; margin-top: 0.5rem;">Booting .NET 10 WebAssembly Engine</p>
</div>
</div>
<div id="blazor-error-ui" style="display: none; position: fixed; bottom: 0; left: 0; right: 0; background: #7f1d1d; color: white; padding: 12px; text-align: center; z-index: 1000;">
An unhandled error has occurred.
<a href="" class="reload" style="color: #fca5a5; font-weight: bold; margin-left: 10px;">Reload</a>
</div>
<script src="js/aquarium.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
+831
View File
@@ -0,0 +1,831 @@
/**
* Network Aquarium Engine
* High-performance 60 FPS HTML5 Canvas Marine Ecosystem
*/
(function () {
window.AquariumEngine = {
canvas: null,
ctx: null,
dotNetRef: null,
creatures: [],
bubbles: [],
caustics: [],
audioContext: null,
soundEnabled: false,
selectedCreature: null,
hoveredCreature: null,
filterMethod: 'ALL',
filterBiome: 'ALL',
searchQuery: '',
animationFrameId: null,
lastTime: 0,
width: 0,
height: 0,
dpr: 1,
// Initialize engine with canvas element and Blazor component reference
init: function (canvasId, dotNetHelper) {
this.canvas = document.getElementById(canvasId);
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
this.dotNetRef = dotNetHelper;
this.dpr = window.devicePixelRatio || 1;
this.resize();
window.addEventListener('resize', () => this.resize());
// Interaction events
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
this.canvas.addEventListener('click', (e) => this.handleClick(e));
this.canvas.addEventListener('mouseleave', () => { this.hoveredCreature = null; });
// Initialize background ambient elements
this.initBackgroundElements();
// Start 60 FPS loop
this.lastTime = performance.now();
this.loop = this.loop.bind(this);
this.animationFrameId = requestAnimationFrame(this.loop);
console.log('🌊 Network Aquarium Engine initialized successfully.');
},
resize: function () {
if (!this.canvas) return;
const rect = this.canvas.getBoundingClientRect();
this.width = rect.width;
this.height = rect.height;
this.canvas.width = this.width * this.dpr;
this.canvas.height = this.height * this.dpr;
this.ctx.scale(this.dpr, this.dpr);
},
initBackgroundElements: function () {
this.bubbles = [];
for (let i = 0; i < 45; i++) {
this.bubbles.push({
x: Math.random() * (this.width || 1200),
y: Math.random() * (this.height || 800),
radius: 1 + Math.random() * 4,
speed: 0.3 + Math.random() * 0.8,
wobbleSpeed: 0.02 + Math.random() * 0.03,
wobbleAmp: 0.5 + Math.random() * 1.5,
opacity: 0.15 + Math.random() * 0.35,
phase: Math.random() * Math.PI * 2
});
}
},
initAudio: function () {
if (!this.audioContext) {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (AudioCtx) {
this.audioContext = new AudioCtx();
}
}
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
},
toggleSound: function () {
this.soundEnabled = !this.soundEnabled;
if (this.soundEnabled) {
this.initAudio();
this.playTone(440, 'sine', 0.1, 0.05);
}
return this.soundEnabled;
},
playTone: function (freq, type, duration, gainLevel = 0.05) {
if (!this.soundEnabled || !this.audioContext) return;
try {
const osc = this.audioContext.createOscillator();
const gain = this.audioContext.createGain();
osc.type = type || 'sine';
osc.frequency.setValueAtTime(freq, this.audioContext.currentTime);
gain.gain.setValueAtTime(gainLevel, this.audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, this.audioContext.currentTime + duration);
osc.connect(gain);
gain.connect(this.audioContext.destination);
osc.start();
osc.stop(this.audioContext.currentTime + duration);
} catch (e) { }
},
playSoundForCreature: function (type) {
if (!this.soundEnabled || !this.audioContext) return;
switch (type) {
case 'Fish':
this.playTone(520 + Math.random() * 200, 'sine', 0.15, 0.03);
break;
case 'Dolphin':
this.playTone(880, 'triangle', 0.25, 0.06);
setTimeout(() => this.playTone(1320, 'sine', 0.2, 0.04), 80);
break;
case 'Whale':
this.playTone(110, 'sine', 1.2, 0.12);
break;
case 'Jellyfish':
this.playTone(220, 'sawtooth', 0.4, 0.07);
break;
case 'Shark':
this.playTone(150, 'sawtooth', 0.35, 0.08);
break;
case 'Crab':
this.playTone(680, 'square', 0.08, 0.02);
break;
}
},
spawnCreature: function (spawnDto) {
if (!spawnDto || !spawnDto.event) return;
const cType = spawnDto.visuals.creatureType || 'Fish';
const depthP = spawnDto.visuals.targetDepthPercent || 0.5;
const targetY = (this.height || 600) * depthP;
const creature = {
id: spawnDto.event.id || Math.random().toString(36).substring(2),
type: cType,
event: spawnDto.event,
visuals: spawnDto.visuals,
x: -120, // Spawn off-screen left
y: targetY + (Math.random() * 40 - 20),
vx: (1.2 + Math.random() * 0.8) * (spawnDto.visuals.speed || 1.0),
vy: 0,
baseY: targetY,
scale: spawnDto.visuals.scale || 1.0,
primaryColor: spawnDto.visuals.primaryColor || '#38bdf8',
secondaryColor: spawnDto.visuals.secondaryColor || '#0284c7',
time: Math.random() * 100,
pulse: 0,
sonarPulses: [],
waterJets: [],
dead: false,
opacity: 1.0
};
// Custom positioning/speed overrides
if (cType === 'Dolphin') {
creature.vx = 2.4;
creature.arcAmp = 45;
creature.arcFreq = 0.03;
} else if (cType === 'Whale') {
creature.vx = 0.65;
creature.x = -240;
} else if (cType === 'Jellyfish') {
creature.vx = 0.5;
creature.vy = -0.3; // floats slightly upward
} else if (cType === 'Shark') {
creature.vx = 3.2;
} else if (cType === 'Crab') {
creature.vx = 0.7;
creature.y = (this.height || 600) - 40;
creature.baseY = (this.height || 600) - 40;
}
this.creatures.push(creature);
this.playSoundForCreature(cType);
// Limit maximum active creatures on canvas to avoid slowdown
if (this.creatures.length > 150) {
this.creatures.shift();
}
},
spawnBatch: function (dtos) {
if (!dtos || !Array.isArray(dtos)) return;
dtos.forEach(d => this.spawnCreature(d));
},
setFilters: function (method, biome, query) {
this.filterMethod = method || 'ALL';
this.filterBiome = biome || 'ALL';
this.searchQuery = (query || '').toLowerCase().trim();
},
clearAll: function () {
this.creatures = [];
this.selectedCreature = null;
this.hoveredCreature = null;
},
// Click / Hit-testing
handleMouseMove: function (e) {
const rect = this.canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
let found = null;
for (let i = this.creatures.length - 1; i >= 0; i--) {
const c = this.creatures[i];
const hitRadius = Math.max(25, 30 * c.scale);
const dx = mx - c.x;
const dy = my - c.y;
if (dx * dx + dy * dy < hitRadius * hitRadius) {
found = c;
break;
}
}
this.hoveredCreature = found;
this.canvas.style.cursor = found ? 'pointer' : 'default';
},
handleClick: function (e) {
if (this.hoveredCreature) {
this.selectedCreature = this.hoveredCreature;
if (this.dotNetRef) {
this.dotNetRef.invokeMethodAsync('OnCreatureSelected', this.selectedCreature.event);
}
this.playTone(800, 'sine', 0.1, 0.04);
}
},
selectCreatureById: function (id) {
const c = this.creatures.find(item => item.id === id);
if (c) {
this.selectedCreature = c;
}
},
// 60 FPS Animation loop
loop: function (currentTime) {
const dt = Math.min(0.1, (currentTime - this.lastTime) / 1000);
this.lastTime = currentTime;
this.update(dt);
this.render();
this.animationFrameId = requestAnimationFrame(this.loop);
},
update: function (dt) {
const w = this.width;
const h = this.height;
// Update ambient bubbles
this.bubbles.forEach(b => {
b.y -= b.speed * 60 * dt;
b.phase += b.wobbleSpeed;
b.currentX = b.x + Math.sin(b.phase) * b.wobbleAmp * 10;
if (b.y < -10) {
b.y = h + 10;
b.x = Math.random() * w;
}
});
// Update creatures
for (let i = this.creatures.length - 1; i >= 0; i--) {
const c = this.creatures[i];
c.time += dt * 5;
// Physics movement
if (c.type === 'Dolphin') {
c.x += c.vx * 60 * dt;
c.y = c.baseY + Math.sin(c.time * c.arcFreq * 10) * c.arcAmp;
// Spawn sonar pulse every 1.5s
if (Math.random() < 0.02) {
c.sonarPulses.push({ radius: 10, maxRadius: 70, opacity: 0.8 });
}
} else if (c.type === 'Whale') {
c.x += c.vx * 60 * dt;
c.y = c.baseY + Math.sin(c.time * 0.15) * 8;
// Water blowhole jet
if (Math.random() < 0.03) {
c.waterJets.push({ yOff: 0, opacity: 0.9 });
}
} else if (c.type === 'Jellyfish') {
c.x += c.vx * 60 * dt;
c.pulse = Math.sin(c.time * 0.8);
c.y += (c.vy + c.pulse * 0.4) * 60 * dt;
} else if (c.type === 'Shark') {
c.x += c.vx * 60 * dt;
c.y = c.baseY + Math.sin(c.time * 0.4) * 12;
} else if (c.type === 'Crab') {
c.x += c.vx * 60 * dt;
c.y = (h - 35) + Math.abs(Math.sin(c.time * 2)) * 3;
} else {
// Regular fish
c.x += c.vx * 60 * dt;
c.y = c.baseY + Math.sin(c.time * 0.6) * 12;
}
// Update sonar pulses
if (c.sonarPulses) {
for (let j = c.sonarPulses.length - 1; j >= 0; j--) {
const sp = c.sonarPulses[j];
sp.radius += 40 * dt;
sp.opacity -= 0.8 * dt;
if (sp.opacity <= 0 || sp.radius >= sp.maxRadius) {
c.sonarPulses.splice(j, 1);
}
}
}
// Remove creatures that fully swam across screen
if (c.x > w + 250) {
this.creatures.splice(i, 1);
if (this.selectedCreature === c) {
this.selectedCreature = null;
}
}
}
},
render: function () {
const ctx = this.ctx;
const w = this.width;
const h = this.height;
if (!ctx || w === 0 || h === 0) return;
// 1. Ocean Background (Gradient Depth)
const oceanGrad = ctx.createLinearGradient(0, 0, 0, h);
oceanGrad.addColorStop(0, '#03203f'); // Sunlight surface turquoise-blue
oceanGrad.addColorStop(0.3, '#02182b'); // Pelagic navy
oceanGrad.addColorStop(0.7, '#01101e'); // Deep twilight
oceanGrad.addColorStop(1, '#00070e'); // Abyssal trench
ctx.fillStyle = oceanGrad;
ctx.fillRect(0, 0, w, h);
// 2. Sunlight Rays & Caustics at Top
this.renderSunlightRays(ctx, w, h);
// 3. Depth Biome Guide Lines (Subtle water layer dividers)
this.renderBiomeDividers(ctx, w, h);
// 4. Ambient Bubbles
ctx.save();
this.bubbles.forEach(b => {
ctx.beginPath();
ctx.arc(b.currentX || b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(186, 230, 253, ${b.opacity})`;
ctx.fill();
});
ctx.restore();
// 5. Ocean Floor & Seaweed
this.renderOceanFloor(ctx, w, h);
// 6. Render All Creatures
this.creatures.forEach(c => {
const isFiltered = this.isCreatureFiltered(c);
if (isFiltered) return;
const isHovered = (this.hoveredCreature === c);
const isSelected = (this.selectedCreature === c);
ctx.save();
ctx.translate(c.x, c.y);
// Highlight glow if hovered/selected
if (isSelected || isHovered) {
ctx.beginPath();
ctx.arc(0, 0, 36 * c.scale, 0, Math.PI * 2);
ctx.strokeStyle = isSelected ? '#38bdf8' : 'rgba(255,255,255,0.6)';
ctx.lineWidth = isSelected ? 2.5 : 1.5;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
// Render specific creature shape
switch (c.type) {
case 'Dolphin':
this.drawDolphin(ctx, c);
break;
case 'Whale':
this.drawWhale(ctx, c);
break;
case 'Jellyfish':
this.drawJellyfish(ctx, c);
break;
case 'Shark':
this.drawShark(ctx, c);
break;
case 'Crab':
this.drawCrab(ctx, c);
break;
default:
this.drawFish(ctx, c);
break;
}
// Render floating status tag
this.drawCreatureBadge(ctx, c, isSelected || isHovered);
ctx.restore();
});
},
renderSunlightRays: function (ctx, w, h) {
ctx.save();
ctx.globalCompositeOperation = 'screen';
const rayGrad = ctx.createLinearGradient(0, 0, w, h * 0.4);
rayGrad.addColorStop(0, 'rgba(56, 189, 248, 0.12)');
rayGrad.addColorStop(0.5, 'rgba(14, 165, 233, 0.04)');
rayGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = rayGrad;
for (let i = 0; i < 5; i++) {
const rayX = (w * 0.2 * i) + Math.sin(this.lastTime * 0.0005 + i) * 30;
ctx.beginPath();
ctx.moveTo(rayX, 0);
ctx.lineTo(rayX + 100, 0);
ctx.lineTo(rayX + 220, h * 0.6);
ctx.lineTo(rayX + 40, h * 0.6);
ctx.closePath();
ctx.fill();
}
ctx.restore();
},
renderBiomeDividers: function (ctx, w, h) {
ctx.save();
ctx.font = '10px "JetBrains Mono", monospace';
ctx.fillStyle = 'rgba(148, 163, 184, 0.25)';
ctx.strokeStyle = 'rgba(148, 163, 184, 0.1)';
ctx.setLineDash([6, 12]);
// Surface / Pelagic line
ctx.beginPath();
ctx.moveTo(20, h * 0.25);
ctx.lineTo(w - 20, h * 0.25);
ctx.stroke();
ctx.fillText('▲ SURFACE / CDN & WEBSOCKETS (0 - 50m)', 30, h * 0.25 - 6);
// Pelagic / Abyssal line
ctx.beginPath();
ctx.moveTo(20, h * 0.75);
ctx.lineTo(w - 20, h * 0.75);
ctx.stroke();
ctx.fillText('▼ ABYSSAL / DATABASE & BACKGROUND WORKERS (200m+)', 30, h * 0.75 - 6);
ctx.restore();
},
renderOceanFloor: function (ctx, w, h) {
ctx.save();
// Sand floor
const floorGrad = ctx.createLinearGradient(0, h - 35, 0, h);
floorGrad.addColorStop(0, '#0a192f');
floorGrad.addColorStop(1, '#020c1b');
ctx.fillStyle = floorGrad;
ctx.beginPath();
ctx.moveTo(0, h);
ctx.lineTo(0, h - 25);
for (let x = 0; x <= w; x += 40) {
ctx.lineTo(x, h - 25 + Math.sin(x * 0.02) * 5);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
// Animated Seaweed
ctx.strokeStyle = '#059669';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
const weedClusters = [60, 180, 340, 520, 720, 940, 1150, 1380];
weedClusters.forEach((wx, idx) => {
if (wx > w) return;
const sway = Math.sin(this.lastTime * 0.0015 + idx) * 12;
ctx.beginPath();
ctx.moveTo(wx, h - 15);
ctx.quadraticCurveTo(wx + sway, h - 55, wx + sway * 1.5, h - 90);
ctx.stroke();
});
ctx.restore();
},
// Creature Renderers
drawFish: function (ctx, c) {
const s = c.scale * 1.0;
const wag = Math.sin(c.time * 2.2) * 6;
// Fish body
ctx.beginPath();
ctx.ellipse(0, 0, 22 * s, 11 * s, 0, 0, Math.PI * 2);
ctx.fillStyle = c.primaryColor;
ctx.shadowColor = c.primaryColor;
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowBlur = 0;
// Belly / accent highlight
ctx.beginPath();
ctx.ellipse(2 * s, 3 * s, 14 * s, 5 * s, 0, 0, Math.PI * 2);
ctx.fillStyle = c.secondaryColor;
ctx.fill();
// Tail fin (oscillating)
ctx.beginPath();
ctx.moveTo(-18 * s, 0);
ctx.lineTo(-32 * s, -10 * s + wag);
ctx.lineTo(-27 * s, 0);
ctx.lineTo(-32 * s, 10 * s + wag);
ctx.closePath();
ctx.fillStyle = c.primaryColor;
ctx.fill();
// Dorsal & Pectoral fin
ctx.beginPath();
ctx.moveTo(-4 * s, -10 * s);
ctx.quadraticCurveTo(2 * s, -18 * s, 8 * s, -10 * s);
ctx.fillStyle = c.secondaryColor;
ctx.fill();
// Eye
ctx.beginPath();
ctx.arc(12 * s, -3 * s, 3.5 * s, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.beginPath();
ctx.arc(13 * s, -3 * s, 1.8 * s, 0, Math.PI * 2);
ctx.fillStyle = '#0f172a';
ctx.fill();
},
drawDolphin: function (ctx, c) {
const s = c.scale * 1.2;
const wag = Math.sin(c.time * 1.8) * 5;
// Sonar waves
if (c.sonarPulses) {
c.sonarPulses.forEach(p => {
ctx.save();
ctx.beginPath();
ctx.arc(20 * s, 0, p.radius, -Math.PI * 0.4, Math.PI * 0.4);
ctx.strokeStyle = `rgba(6, 182, 212, ${p.opacity})`;
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
});
}
// Sleek Dolphin body
ctx.beginPath();
ctx.moveTo(32 * s, 0); // Snout/Beak
ctx.quadraticCurveTo(15 * s, -14 * s, -12 * s, -8 * s);
ctx.quadraticCurveTo(-28 * s, -4 * s, -40 * s, wag);
ctx.quadraticCurveTo(-25 * s, 10 * s, 5 * s, 10 * s);
ctx.quadraticCurveTo(24 * s, 6 * s, 32 * s, 0);
ctx.closePath();
ctx.fillStyle = '#06b6d4';
ctx.shadowColor = '#0891b2';
ctx.shadowBlur = 12;
ctx.fill();
ctx.shadowBlur = 0;
// Belly
ctx.beginPath();
ctx.moveTo(25 * s, 2 * s);
ctx.quadraticCurveTo(6 * s, 8 * s, -20 * s, 6 * s);
ctx.quadraticCurveTo(-5 * s, 0, 25 * s, 2 * s);
ctx.fillStyle = '#e0f2fe';
ctx.fill();
// Dorsal fin
ctx.beginPath();
ctx.moveTo(-2 * s, -12 * s);
ctx.quadraticCurveTo(4 * s, -24 * s, 12 * s, -11 * s);
ctx.fillStyle = '#0891b2';
ctx.fill();
// Tail fluke
ctx.beginPath();
ctx.moveTo(-38 * s, wag);
ctx.lineTo(-50 * s, -12 * s + wag);
ctx.lineTo(-45 * s, wag);
ctx.lineTo(-50 * s, 12 * s + wag);
ctx.closePath();
ctx.fillStyle = '#06b6d4';
ctx.fill();
// Eye
ctx.beginPath();
ctx.arc(18 * s, -4 * s, 2.5 * s, 0, Math.PI * 2);
ctx.fillStyle = '#0f172a';
ctx.fill();
},
drawWhale: function (ctx, c) {
const s = c.scale * 1.5;
const wag = Math.sin(c.time * 0.8) * 6;
// Giant Whale Body
ctx.beginPath();
ctx.moveTo(55 * s, -4 * s);
ctx.quadraticCurveTo(25 * s, -26 * s, -35 * s, -15 * s);
ctx.quadraticCurveTo(-65 * s, -6 * s, -85 * s, wag);
ctx.quadraticCurveTo(-55 * s, 24 * s, 0 * s, 22 * s);
ctx.quadraticCurveTo(42 * s, 18 * s, 55 * s, -4 * s);
ctx.closePath();
ctx.fillStyle = '#1d4ed8';
ctx.shadowColor = '#3b82f6';
ctx.shadowBlur = 16;
ctx.fill();
ctx.shadowBlur = 0;
// Belly grooves
ctx.strokeStyle = '#60a5fa';
ctx.lineWidth = 1.5;
for (let g = 0; g < 4; g++) {
ctx.beginPath();
ctx.moveTo(35 * s - g * 10 * s, 12 * s);
ctx.quadraticCurveTo(10 * s - g * 8 * s, 18 * s, -15 * s - g * 12 * s, 10 * s);
ctx.stroke();
}
// Flukes
ctx.beginPath();
ctx.moveTo(-82 * s, wag);
ctx.lineTo(-102 * s, -22 * s + wag);
ctx.lineTo(-94 * s, wag);
ctx.lineTo(-102 * s, 22 * s + wag);
ctx.closePath();
ctx.fillStyle = '#1e40af';
ctx.fill();
// Tiny eye
ctx.beginPath();
ctx.arc(38 * s, -6 * s, 2.5 * s, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.beginPath();
ctx.arc(39 * s, -6 * s, 1.2 * s, 0, Math.PI * 2);
ctx.fillStyle = '#020617';
ctx.fill();
},
drawJellyfish: function (ctx, c) {
const s = c.scale * 1.2;
const p = c.pulse || 0;
const isDanger = c.visuals.isDangerous;
const col = isDanger ? '#ef4444' : '#f59e0b';
// Pulsing translucent Bell/Dome
ctx.beginPath();
ctx.arc(0, -8 * s, 20 * s + p * 3, Math.PI, 0);
ctx.quadraticCurveTo(15 * s, 4 * s + p * 2, 0, 2 * s);
ctx.quadraticCurveTo(-15 * s, 4 * s + p * 2, -20 * s - p * 3, -8 * s);
ctx.fillStyle = isDanger ? 'rgba(239, 68, 68, 0.75)' : 'rgba(245, 158, 11, 0.75)';
ctx.shadowColor = col;
ctx.shadowBlur = 18;
ctx.fill();
ctx.shadowBlur = 0;
// Warning Inner Core
ctx.beginPath();
ctx.arc(0, -10 * s, 8 * s, 0, Math.PI * 2);
ctx.fillStyle = isDanger ? '#fee2e2' : '#fef3c7';
ctx.fill();
// Undulating Tentacles
ctx.strokeStyle = col;
ctx.lineWidth = 1.8;
ctx.lineCap = 'round';
for (let t = -3; t <= 3; t++) {
const tx = t * 5 * s;
const wave = Math.sin(c.time * 1.5 + t) * 8;
ctx.beginPath();
ctx.moveTo(tx, 0);
ctx.quadraticCurveTo(tx + wave, 18 * s, tx - wave, 36 * s);
ctx.stroke();
}
},
drawShark: function (ctx, c) {
const s = c.scale * 1.3;
const wag = Math.sin(c.time * 2.5) * 8;
// Predator Shark Body
ctx.beginPath();
ctx.moveTo(42 * s, 0);
ctx.quadraticCurveTo(15 * s, -16 * s, -20 * s, -10 * s);
ctx.lineTo(-55 * s, wag);
ctx.quadraticCurveTo(-25 * s, 14 * s, 10 * s, 12 * s);
ctx.closePath();
ctx.fillStyle = '#334155';
ctx.shadowColor = '#e11d48';
ctx.shadowBlur = 14;
ctx.fill();
ctx.shadowBlur = 0;
// Sharp Dorsal Fin
ctx.beginPath();
ctx.moveTo(2 * s, -14 * s);
ctx.lineTo(12 * s, -30 * s);
ctx.lineTo(18 * s, -12 * s);
ctx.closePath();
ctx.fillStyle = '#1e293b';
ctx.fill();
// Shark Tail
ctx.beginPath();
ctx.moveTo(-50 * s, wag);
ctx.lineTo(-68 * s, -18 * s + wag);
ctx.lineTo(-60 * s, wag);
ctx.lineTo(-68 * s, 14 * s + wag);
ctx.closePath();
ctx.fillStyle = '#334155';
ctx.fill();
// Threatening red eye & gills
ctx.beginPath();
ctx.arc(28 * s, -3 * s, 2.5 * s, 0, Math.PI * 2);
ctx.fillStyle = '#e11d48';
ctx.fill();
},
drawCrab: function (ctx, c) {
const s = c.scale * 1.0;
const legWiggle = Math.sin(c.time * 4) * 4;
// Crab Body/Shell
ctx.beginPath();
ctx.ellipse(0, 0, 16 * s, 10 * s, 0, 0, Math.PI * 2);
ctx.fillStyle = '#ea580c';
ctx.shadowColor = '#f97316';
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowBlur = 0;
// Claws
ctx.strokeStyle = '#c2410c';
ctx.lineWidth = 3;
// Left claw
ctx.beginPath();
ctx.moveTo(-10 * s, -4 * s);
ctx.lineTo(-18 * s, -14 * s);
ctx.stroke();
// Right claw
ctx.beginPath();
ctx.moveTo(10 * s, -4 * s);
ctx.lineTo(18 * s, -14 * s);
ctx.stroke();
// Legs
ctx.lineWidth = 2;
for (let l = -2; l <= 2; l++) {
if (l === 0) continue;
ctx.beginPath();
ctx.moveTo(l * 6 * s, 4 * s);
ctx.lineTo(l * 10 * s, 14 * s + legWiggle);
ctx.stroke();
}
// Stalk eyes
ctx.beginPath();
ctx.arc(-5 * s, -11 * s, 2 * s, 0, Math.PI * 2);
ctx.arc(5 * s, -11 * s, 2 * s, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
},
drawCreatureBadge: function (ctx, c, isEmphasized) {
const label = c.visuals.displayLabel || `${c.event.method} ${c.event.statusCode}`;
const s = c.scale;
const tagY = -24 * s - (c.type === 'Whale' ? 14 : 0);
ctx.save();
ctx.font = isEmphasized ? 'bold 11px "JetBrains Mono", monospace' : '10px "JetBrains Mono", monospace';
const metrics = ctx.measureText(label);
const padX = 6;
const tagW = metrics.width + padX * 2;
const tagH = 16;
// Badge Background pill
ctx.beginPath();
ctx.roundRect(-tagW / 2, tagY - tagH / 2, tagW, tagH, 4);
ctx.fillStyle = isEmphasized ? 'rgba(15, 23, 42, 0.95)' : 'rgba(15, 23, 42, 0.65)';
ctx.fill();
ctx.strokeStyle = isEmphasized ? c.primaryColor : 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 1;
ctx.stroke();
// Badge Text
ctx.fillStyle = isEmphasized ? '#ffffff' : '#cbd5e1';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, 0, tagY);
ctx.restore();
},
isCreatureFiltered: function (c) {
if (this.filterMethod !== 'ALL' && c.event.method !== this.filterMethod) {
return true;
}
if (this.filterBiome !== 'ALL' && c.visuals.biome !== this.filterBiome) {
return true;
}
if (this.searchQuery && !c.event.path.toLowerCase().includes(this.searchQuery)) {
return true;
}
return false;
}
};
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
[
{
"date": "2022-01-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2022-01-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2022-01-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2022-01-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2022-01-10",
"temperatureC": -2,
"summary": "Chilly"
}
]