added aquarium
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 · .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 >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">×</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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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
+1
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
+1
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 */
|
||||
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5393
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
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"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user