Compare commits
No commits in common. 'main' and 'master' have entirely different histories.
74 changed files with 61507 additions and 398 deletions
@ -1,400 +1,17 @@ |
|||||||
# ---> VisualStudio |
# VS / Rider |
||||||
## Ignore Visual Studio temporary files, build results, and |
|
||||||
## files generated by popular Visual Studio add-ons. |
|
||||||
## |
|
||||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore |
|
||||||
|
|
||||||
# User-specific files |
|
||||||
*.rsuser |
|
||||||
*.suo |
|
||||||
*.user |
|
||||||
*.userosscache |
|
||||||
*.sln.docstates |
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio) |
|
||||||
*.userprefs |
|
||||||
|
|
||||||
# Mono auto generated files |
|
||||||
mono_crash.* |
|
||||||
|
|
||||||
# Build results |
|
||||||
[Dd]ebug/ |
|
||||||
[Dd]ebugPublic/ |
|
||||||
[Rr]elease/ |
|
||||||
[Rr]eleases/ |
|
||||||
x64/ |
|
||||||
x86/ |
|
||||||
[Ww][Ii][Nn]32/ |
|
||||||
[Aa][Rr][Mm]/ |
|
||||||
[Aa][Rr][Mm]64/ |
|
||||||
bld/ |
|
||||||
[Bb]in/ |
|
||||||
[Oo]bj/ |
|
||||||
[Ll]og/ |
|
||||||
[Ll]ogs/ |
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory |
|
||||||
.vs/ |
.vs/ |
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot |
.idea/ |
||||||
#wwwroot/ |
*.user |
||||||
|
*.suo |
||||||
# Visual Studio 2017 auto generated files |
|
||||||
Generated\ Files/ |
|
||||||
|
|
||||||
# MSTest test Results |
|
||||||
[Tt]est[Rr]esult*/ |
|
||||||
[Bb]uild[Ll]og.* |
|
||||||
|
|
||||||
# NUnit |
|
||||||
*.VisualState.xml |
|
||||||
TestResult.xml |
|
||||||
nunit-*.xml |
|
||||||
|
|
||||||
# Build Results of an ATL Project |
|
||||||
[Dd]ebugPS/ |
|
||||||
[Rr]eleasePS/ |
|
||||||
dlldata.c |
|
||||||
|
|
||||||
# Benchmark Results |
|
||||||
BenchmarkDotNet.Artifacts/ |
|
||||||
|
|
||||||
# .NET Core |
|
||||||
project.lock.json |
|
||||||
project.fragment.lock.json |
|
||||||
artifacts/ |
|
||||||
|
|
||||||
# ASP.NET Scaffolding |
|
||||||
ScaffoldingReadMe.txt |
|
||||||
|
|
||||||
# StyleCop |
|
||||||
StyleCopReport.xml |
|
||||||
|
|
||||||
# Files built by Visual Studio |
|
||||||
*_i.c |
|
||||||
*_p.c |
|
||||||
*_h.h |
|
||||||
*.ilk |
|
||||||
*.meta |
|
||||||
*.obj |
|
||||||
*.iobj |
|
||||||
*.pch |
|
||||||
*.pdb |
|
||||||
*.ipdb |
|
||||||
*.pgc |
|
||||||
*.pgd |
|
||||||
*.rsp |
|
||||||
*.sbr |
|
||||||
*.tlb |
|
||||||
*.tli |
|
||||||
*.tlh |
|
||||||
*.tmp |
|
||||||
*.tmp_proj |
|
||||||
*_wpftmp.csproj |
|
||||||
*.log |
|
||||||
*.tlog |
|
||||||
*.vspscc |
|
||||||
*.vssscc |
|
||||||
.builds |
|
||||||
*.pidb |
|
||||||
*.svclog |
|
||||||
*.scc |
|
||||||
|
|
||||||
# Chutzpah Test files |
|
||||||
_Chutzpah* |
|
||||||
|
|
||||||
# Visual C++ cache files |
|
||||||
ipch/ |
|
||||||
*.aps |
|
||||||
*.ncb |
|
||||||
*.opendb |
|
||||||
*.opensdf |
|
||||||
*.sdf |
|
||||||
*.cachefile |
|
||||||
*.VC.db |
|
||||||
*.VC.VC.opendb |
|
||||||
|
|
||||||
# Visual Studio profiler |
|
||||||
*.psess |
|
||||||
*.vsp |
|
||||||
*.vspx |
|
||||||
*.sap |
|
||||||
|
|
||||||
# Visual Studio Trace Files |
|
||||||
*.e2e |
|
||||||
|
|
||||||
# TFS 2012 Local Workspace |
|
||||||
$tf/ |
|
||||||
|
|
||||||
# Guidance Automation Toolkit |
|
||||||
*.gpState |
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in |
|
||||||
_ReSharper*/ |
|
||||||
*.[Rr]e[Ss]harper |
|
||||||
*.DotSettings.user |
|
||||||
|
|
||||||
# TeamCity is a build add-in |
|
||||||
_TeamCity* |
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool |
|
||||||
*.dotCover |
|
||||||
|
|
||||||
# AxoCover is a Code Coverage Tool |
|
||||||
.axoCover/* |
|
||||||
!.axoCover/settings.json |
|
||||||
|
|
||||||
# Coverlet is a free, cross platform Code Coverage Tool |
|
||||||
coverage*.json |
|
||||||
coverage*.xml |
|
||||||
coverage*.info |
|
||||||
|
|
||||||
# Visual Studio code coverage results |
|
||||||
*.coverage |
|
||||||
*.coveragexml |
|
||||||
|
|
||||||
# NCrunch |
|
||||||
_NCrunch_* |
|
||||||
.*crunch*.local.xml |
|
||||||
nCrunchTemp_* |
|
||||||
|
|
||||||
# MightyMoose |
|
||||||
*.mm.* |
|
||||||
AutoTest.Net/ |
|
||||||
|
|
||||||
# Web workbench (sass) |
|
||||||
.sass-cache/ |
|
||||||
|
|
||||||
# Installshield output folder |
|
||||||
[Ee]xpress/ |
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in |
|
||||||
DocProject/buildhelp/ |
|
||||||
DocProject/Help/*.HxT |
|
||||||
DocProject/Help/*.HxC |
|
||||||
DocProject/Help/*.hhc |
|
||||||
DocProject/Help/*.hhk |
|
||||||
DocProject/Help/*.hhp |
|
||||||
DocProject/Help/Html2 |
|
||||||
DocProject/Help/html |
|
||||||
|
|
||||||
# Click-Once directory |
|
||||||
publish/ |
|
||||||
|
|
||||||
# Publish Web Output |
|
||||||
*.[Pp]ublish.xml |
|
||||||
*.azurePubxml |
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings, |
|
||||||
# but database connection strings (with potential passwords) will be unencrypted |
|
||||||
*.pubxml |
|
||||||
*.publishproj |
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to |
# Build output |
||||||
# checkin your Azure Web App publish settings, but sensitive information contained |
bin/ |
||||||
# in these scripts will be unencrypted |
obj/ |
||||||
PublishScripts/ |
|
||||||
|
|
||||||
# NuGet Packages |
# NuGet |
||||||
*.nupkg |
*.nupkg |
||||||
# NuGet Symbol Packages |
packages/ |
||||||
*.snupkg |
|
||||||
# The packages folder can be ignored because of Package Restore |
|
||||||
**/[Pp]ackages/* |
|
||||||
# except build/, which is used as an MSBuild target. |
|
||||||
!**/[Pp]ackages/build/ |
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed |
|
||||||
#!**/[Pp]ackages/repositories.config |
|
||||||
# NuGet v3's project.json files produces more ignorable files |
|
||||||
*.nuget.props |
|
||||||
*.nuget.targets |
|
||||||
|
|
||||||
# Microsoft Azure Build Output |
|
||||||
csx/ |
|
||||||
*.build.csdef |
|
||||||
|
|
||||||
# Microsoft Azure Emulator |
|
||||||
ecf/ |
|
||||||
rcf/ |
|
||||||
|
|
||||||
# Windows Store app package directories and files |
|
||||||
AppPackages/ |
|
||||||
BundleArtifacts/ |
|
||||||
Package.StoreAssociation.xml |
|
||||||
_pkginfo.txt |
|
||||||
*.appx |
|
||||||
*.appxbundle |
|
||||||
*.appxupload |
|
||||||
|
|
||||||
# Visual Studio cache files |
|
||||||
# files ending in .cache can be ignored |
|
||||||
*.[Cc]ache |
|
||||||
# but keep track of directories ending in .cache |
|
||||||
!?*.[Cc]ache/ |
|
||||||
|
|
||||||
# Others |
|
||||||
ClientBin/ |
|
||||||
~$* |
|
||||||
*~ |
|
||||||
*.dbmdl |
|
||||||
*.dbproj.schemaview |
|
||||||
*.jfm |
|
||||||
*.pfx |
|
||||||
*.publishsettings |
|
||||||
orleans.codegen.cs |
|
||||||
|
|
||||||
# Including strong name files can present a security risk |
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424) |
|
||||||
#*.snk |
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components |
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) |
|
||||||
#bower_components/ |
|
||||||
|
|
||||||
# RIA/Silverlight projects |
|
||||||
Generated_Code/ |
|
||||||
|
|
||||||
# Backup & report files from converting an old project file |
|
||||||
# to a newer Visual Studio version. Backup files are not needed, |
|
||||||
# because we have git ;-) |
|
||||||
_UpgradeReport_Files/ |
|
||||||
Backup*/ |
|
||||||
UpgradeLog*.XML |
|
||||||
UpgradeLog*.htm |
|
||||||
ServiceFabricBackup/ |
|
||||||
*.rptproj.bak |
|
||||||
|
|
||||||
# SQL Server files |
|
||||||
*.mdf |
|
||||||
*.ldf |
|
||||||
*.ndf |
|
||||||
|
|
||||||
# Business Intelligence projects |
|
||||||
*.rdl.data |
|
||||||
*.bim.layout |
|
||||||
*.bim_*.settings |
|
||||||
*.rptproj.rsuser |
|
||||||
*- [Bb]ackup.rdl |
|
||||||
*- [Bb]ackup ([0-9]).rdl |
|
||||||
*- [Bb]ackup ([0-9][0-9]).rdl |
|
||||||
|
|
||||||
# Microsoft Fakes |
|
||||||
FakesAssemblies/ |
|
||||||
|
|
||||||
# GhostDoc plugin setting file |
|
||||||
*.GhostDoc.xml |
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio |
|
||||||
.ntvs_analysis.dat |
|
||||||
node_modules/ |
|
||||||
|
|
||||||
# Visual Studio 6 build log |
|
||||||
*.plg |
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file |
|
||||||
*.opt |
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) |
|
||||||
*.vbw |
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.) |
|
||||||
*.vbp |
|
||||||
|
|
||||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project) |
|
||||||
*.dsw |
|
||||||
*.dsp |
|
||||||
|
|
||||||
# Visual Studio 6 technical files |
|
||||||
*.ncb |
|
||||||
*.aps |
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output |
|
||||||
**/*.HTMLClient/GeneratedArtifacts |
|
||||||
**/*.DesktopClient/GeneratedArtifacts |
|
||||||
**/*.DesktopClient/ModelManifest.xml |
|
||||||
**/*.Server/GeneratedArtifacts |
|
||||||
**/*.Server/ModelManifest.xml |
|
||||||
_Pvt_Extensions |
|
||||||
|
|
||||||
# Paket dependency manager |
|
||||||
.paket/paket.exe |
|
||||||
paket-files/ |
|
||||||
|
|
||||||
# FAKE - F# Make |
|
||||||
.fake/ |
|
||||||
|
|
||||||
# CodeRush personal settings |
|
||||||
.cr/personal |
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS) |
|
||||||
__pycache__/ |
|
||||||
*.pyc |
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it |
|
||||||
# tools/** |
|
||||||
# !tools/packages.config |
|
||||||
|
|
||||||
# Tabs Studio |
|
||||||
*.tss |
|
||||||
|
|
||||||
# Telerik's JustMock configuration file |
|
||||||
*.jmconfig |
|
||||||
|
|
||||||
# BizTalk build output |
|
||||||
*.btp.cs |
|
||||||
*.btm.cs |
|
||||||
*.odx.cs |
|
||||||
*.xsd.cs |
|
||||||
|
|
||||||
# OpenCover UI analysis results |
|
||||||
OpenCover/ |
|
||||||
|
|
||||||
# Azure Stream Analytics local run output |
|
||||||
ASALocalRun/ |
|
||||||
|
|
||||||
# MSBuild Binary and Structured Log |
|
||||||
*.binlog |
|
||||||
|
|
||||||
# NVidia Nsight GPU debugger configuration file |
|
||||||
*.nvuser |
|
||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder |
|
||||||
.mfractor/ |
|
||||||
|
|
||||||
# Local History for Visual Studio |
|
||||||
.localhistory/ |
|
||||||
|
|
||||||
# Visual Studio History (VSHistory) files |
|
||||||
.vshistory/ |
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database |
|
||||||
healthchecksdb |
|
||||||
|
|
||||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017 |
|
||||||
MigrationBackup/ |
|
||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder |
|
||||||
.ionide/ |
|
||||||
|
|
||||||
# Fody - auto-generated XML schema |
|
||||||
FodyWeavers.xsd |
|
||||||
|
|
||||||
# VS Code files for those working on multiple tools |
|
||||||
.vscode/* |
|
||||||
!.vscode/settings.json |
|
||||||
!.vscode/tasks.json |
|
||||||
!.vscode/launch.json |
|
||||||
!.vscode/extensions.json |
|
||||||
*.code-workspace |
|
||||||
|
|
||||||
# Local History for Visual Studio Code |
|
||||||
.history/ |
|
||||||
|
|
||||||
# Windows Installer files from build outputs |
|
||||||
*.cab |
|
||||||
*.msi |
|
||||||
*.msix |
|
||||||
*.msm |
|
||||||
*.msp |
|
||||||
|
|
||||||
# JetBrains Rider |
|
||||||
*.sln.iml |
|
||||||
|
|
||||||
|
# OS |
||||||
|
Thumbs.db |
||||||
|
.DS_Store |
||||||
|
|||||||
@ -0,0 +1,3 @@ |
|||||||
|
<Solution> |
||||||
|
<Project Path="IOModuleTestBlazor/IOModuleTestBlazor.csproj" /> |
||||||
|
</Solution> |
||||||
@ -0,0 +1,147 @@ |
|||||||
|
using Peak.Can.Basic; |
||||||
|
using IOModuleTestBlazor.Models; |
||||||
|
using IOModuleTestBlazor.Services; |
||||||
|
|
||||||
|
namespace IOModuleTestBlazor; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Background service that initialises the PCAN adapter and streams CAN messages |
||||||
|
/// to the rest of the app via <see cref="ICanService.MessageReceived"/>. |
||||||
|
/// </summary> |
||||||
|
public class CanWorker( |
||||||
|
ILogger<CanWorker> logger, |
||||||
|
IConfiguration configuration, |
||||||
|
ICanService canService) : BackgroundService |
||||||
|
{ |
||||||
|
public override Task StartAsync(CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
// Seed filters and bitmasks from appsettings regardless of connection state |
||||||
|
foreach (var f in configuration.GetSection("CanOptions:Filters").Get<CanFilter[]>() ?? []) |
||||||
|
canService.AddFilter(f); |
||||||
|
foreach (var b in configuration.GetSection("CanOptions:Bitmasks").Get<CanBitmask[]>() ?? []) |
||||||
|
canService.AddBitmask(b); |
||||||
|
|
||||||
|
// Auto-connect if config is present; failures are non-fatal so the app |
||||||
|
// still starts and the user can configure the connection from the UI. |
||||||
|
try |
||||||
|
{ |
||||||
|
var channel = ResolveChannel(); |
||||||
|
var bitrate = ResolveBitrate(); |
||||||
|
canService.Initialize(channel, bitrate); |
||||||
|
logger.LogInformation("CAN channel {Channel} ready at {Bitrate}", channel, bitrate); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
logger.LogWarning(ex, "CAN auto-connect failed — configure via the UI."); |
||||||
|
} |
||||||
|
|
||||||
|
return base.StartAsync(cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
||||||
|
{ |
||||||
|
while (!stoppingToken.IsCancellationRequested) |
||||||
|
{ |
||||||
|
// Pick up any reinit request from the UI |
||||||
|
if (canService.TryConsumePendingReinit(out var newChannel, out var newBitrate)) |
||||||
|
{ |
||||||
|
logger.LogInformation("Reinitializing CAN: {Channel} at {Bitrate}", newChannel, newBitrate); |
||||||
|
if (canService.IsConnected) canService.Uninitialize(); |
||||||
|
try |
||||||
|
{ |
||||||
|
canService.Initialize(newChannel, newBitrate); |
||||||
|
logger.LogInformation("CAN reinitialized: {Channel} at {Bitrate}", newChannel, newBitrate); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
logger.LogError(ex, "CAN reinitialization failed."); |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (!canService.IsConnected) |
||||||
|
{ |
||||||
|
await Task.Delay(100, stoppingToken); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
var result = canService.Read(out PcanMessage msg, out ulong timestamp); |
||||||
|
|
||||||
|
if (result == PcanStatus.OK) |
||||||
|
{ |
||||||
|
if (!canService.PassesFilter(msg.ID)) |
||||||
|
continue; |
||||||
|
|
||||||
|
var data = new byte[msg.DLC]; |
||||||
|
for (int i = 0; i < msg.DLC; i++) |
||||||
|
data[i] = msg.Data[i]; |
||||||
|
|
||||||
|
var signals = canService.ExtractSignals(msg.ID, data); |
||||||
|
|
||||||
|
canService.PublishMessage(new CanMessageDto( |
||||||
|
Id: msg.ID, |
||||||
|
Data: data, |
||||||
|
Dlc: msg.DLC, |
||||||
|
TimestampUs: timestamp, |
||||||
|
IsExtended: msg.MsgType == MessageType.Extended, |
||||||
|
Signals: signals.Count > 0 ? signals : null |
||||||
|
)); |
||||||
|
|
||||||
|
logger.LogDebug("[RX] ID=0x{Id:X3} DLC={Dlc} Data={Data}", |
||||||
|
msg.ID, msg.DLC, Convert.ToHexString(data)); |
||||||
|
} |
||||||
|
else if (result == PcanStatus.ReceiveQueueEmpty) |
||||||
|
{ |
||||||
|
await Task.Delay(1, stoppingToken); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
logger.LogError("CAN read error: {Error}", GetErrorText(result)); |
||||||
|
await Task.Delay(100, stoppingToken); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public override Task StopAsync(CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
canService.Uninitialize(); |
||||||
|
logger.LogInformation("CAN channel uninitialized."); |
||||||
|
return base.StopAsync(cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
// ── Channel / bitrate resolution ────────────────────────────────────────── |
||||||
|
|
||||||
|
private PcanChannel ResolveChannel() |
||||||
|
{ |
||||||
|
var name = configuration["CanOptions:Channel"]; |
||||||
|
if (!string.IsNullOrWhiteSpace(name) && |
||||||
|
Enum.TryParse(name, ignoreCase: true, out PcanChannel configured)) |
||||||
|
return configured; |
||||||
|
|
||||||
|
logger.LogInformation("No channel configured — scanning for available PCAN USB channels..."); |
||||||
|
var available = canService.GetAvailableChannels(); |
||||||
|
|
||||||
|
if (available.Count == 0) |
||||||
|
throw new InvalidOperationException("No PCAN USB channels detected."); |
||||||
|
|
||||||
|
logger.LogInformation("Auto-selected channel: {Channel}", available[0]); |
||||||
|
return available[0]; |
||||||
|
} |
||||||
|
|
||||||
|
private Bitrate ResolveBitrate() |
||||||
|
{ |
||||||
|
var name = configuration["CanOptions:Bitrate"]; |
||||||
|
if (!string.IsNullOrWhiteSpace(name) && |
||||||
|
Enum.TryParse(name, ignoreCase: true, out Bitrate configured)) |
||||||
|
return configured; |
||||||
|
|
||||||
|
logger.LogInformation("No bitrate configured — defaulting to 500 kbps."); |
||||||
|
return Bitrate.Pcan500; |
||||||
|
} |
||||||
|
|
||||||
|
private static string GetErrorText(PcanStatus status) |
||||||
|
{ |
||||||
|
try { Api.GetErrorText(status, out var text); return text; } |
||||||
|
catch (PcanBasicException) { return status.ToString(); } |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
.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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#blazor-error-ui { |
||||||
|
color-scheme: light only; |
||||||
|
background: lightyellow; |
||||||
|
bottom: 0; |
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); |
||||||
|
box-sizing: border-box; |
||||||
|
display: none; |
||||||
|
left: 0; |
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem; |
||||||
|
position: fixed; |
||||||
|
width: 100%; |
||||||
|
z-index: 1000; |
||||||
|
} |
||||||
|
|
||||||
|
#blazor-error-ui .dismiss { |
||||||
|
cursor: pointer; |
||||||
|
position: absolute; |
||||||
|
right: 0.75rem; |
||||||
|
top: 0.5rem; |
||||||
|
} |
||||||
@ -0,0 +1,105 @@ |
|||||||
|
.navbar-toggler { |
||||||
|
appearance: none; |
||||||
|
cursor: pointer; |
||||||
|
width: 3.5rem; |
||||||
|
height: 2.5rem; |
||||||
|
color: white; |
||||||
|
position: absolute; |
||||||
|
top: 0.5rem; |
||||||
|
right: 1rem; |
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1); |
||||||
|
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); |
||||||
|
} |
||||||
|
|
||||||
|
.navbar-toggler:checked { |
||||||
|
background-color: rgba(255, 255, 255, 0.5); |
||||||
|
} |
||||||
|
|
||||||
|
.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 .nav-link { |
||||||
|
color: #d7d7d7; |
||||||
|
background: none; |
||||||
|
border: none; |
||||||
|
border-radius: 4px; |
||||||
|
height: 3rem; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
line-height: 3rem; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.nav-item ::deep a.active { |
||||||
|
background-color: rgba(255,255,255,0.37); |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.nav-item ::deep .nav-link:hover { |
||||||
|
background-color: rgba(255,255,255,0.1); |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.nav-scrollable { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
.navbar-toggler:checked ~ .nav-scrollable { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
|
||||||
|
@media (min-width: 641px) { |
||||||
|
.navbar-toggler { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
.nav-scrollable { |
||||||
|
/* Never collapse the sidebar for wide screens */ |
||||||
|
display: block; |
||||||
|
|
||||||
|
/* Allow sidebar to scroll for tall menus */ |
||||||
|
height: calc(100vh - 3.5rem); |
||||||
|
overflow-y: auto; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,157 @@ |
|||||||
|
.components-reconnect-first-attempt-visible, |
||||||
|
.components-reconnect-repeated-attempt-visible, |
||||||
|
.components-reconnect-failed-visible, |
||||||
|
.components-pause-visible, |
||||||
|
.components-resume-failed-visible, |
||||||
|
.components-rejoining-animation { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, |
||||||
|
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, |
||||||
|
#components-reconnect-modal.components-reconnect-paused .components-pause-visible, |
||||||
|
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, |
||||||
|
#components-reconnect-modal.components-reconnect-retrying, |
||||||
|
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, |
||||||
|
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, |
||||||
|
#components-reconnect-modal.components-reconnect-failed, |
||||||
|
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#components-reconnect-modal { |
||||||
|
background-color: white; |
||||||
|
width: 20rem; |
||||||
|
margin: 20vh auto; |
||||||
|
padding: 2rem; |
||||||
|
border: 0; |
||||||
|
border-radius: 0.5rem; |
||||||
|
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); |
||||||
|
opacity: 0; |
||||||
|
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; |
||||||
|
animation: components-reconnect-modal-fadeOutOpacity 0.5s both; |
||||||
|
&[open] |
||||||
|
|
||||||
|
{ |
||||||
|
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; |
||||||
|
animation-fill-mode: both; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
#components-reconnect-modal::backdrop { |
||||||
|
background-color: rgba(0, 0, 0, 0.4); |
||||||
|
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes components-reconnect-modal-slideUp { |
||||||
|
0% { |
||||||
|
transform: translateY(30px) scale(0.95); |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
transform: translateY(0); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes components-reconnect-modal-fadeInOpacity { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes components-reconnect-modal-fadeOutOpacity { |
||||||
|
0% { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.components-reconnect-container { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
gap: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
#components-reconnect-modal p { |
||||||
|
margin: 0; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
#components-reconnect-modal button { |
||||||
|
border: 0; |
||||||
|
background-color: #6b9ed2; |
||||||
|
color: white; |
||||||
|
padding: 4px 24px; |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
#components-reconnect-modal button:hover { |
||||||
|
background-color: #3b6ea2; |
||||||
|
} |
||||||
|
|
||||||
|
#components-reconnect-modal button:active { |
||||||
|
background-color: #6b9ed2; |
||||||
|
} |
||||||
|
|
||||||
|
.components-rejoining-animation { |
||||||
|
position: relative; |
||||||
|
width: 80px; |
||||||
|
height: 80px; |
||||||
|
} |
||||||
|
|
||||||
|
.components-rejoining-animation div { |
||||||
|
position: absolute; |
||||||
|
border: 3px solid #0087ff; |
||||||
|
opacity: 1; |
||||||
|
border-radius: 50%; |
||||||
|
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; |
||||||
|
} |
||||||
|
|
||||||
|
.components-rejoining-animation div:nth-child(2) { |
||||||
|
animation-delay: -0.5s; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes components-rejoining-animation { |
||||||
|
0% { |
||||||
|
top: 40px; |
||||||
|
left: 40px; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
|
||||||
|
4.9% { |
||||||
|
top: 40px; |
||||||
|
left: 40px; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
|
||||||
|
5% { |
||||||
|
top: 40px; |
||||||
|
left: 40px; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
top: 0px; |
||||||
|
left: 0px; |
||||||
|
width: 80px; |
||||||
|
height: 80px; |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
// Set up event handlers
|
||||||
|
const reconnectModal = document.getElementById("components-reconnect-modal"); |
||||||
|
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); |
||||||
|
|
||||||
|
const retryButton = document.getElementById("components-reconnect-button"); |
||||||
|
retryButton.addEventListener("click", retry); |
||||||
|
|
||||||
|
const resumeButton = document.getElementById("components-resume-button"); |
||||||
|
resumeButton.addEventListener("click", resume); |
||||||
|
|
||||||
|
function handleReconnectStateChanged(event) { |
||||||
|
if (event.detail.state === "show") { |
||||||
|
reconnectModal.showModal(); |
||||||
|
} else if (event.detail.state === "hide") { |
||||||
|
reconnectModal.close(); |
||||||
|
} else if (event.detail.state === "failed") { |
||||||
|
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); |
||||||
|
} else if (event.detail.state === "rejected") { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function retry() { |
||||||
|
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); |
||||||
|
|
||||||
|
try { |
||||||
|
// Reconnect will asynchronously return:
|
||||||
|
// - true to mean success
|
||||||
|
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||||
|
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||||
|
const successful = await Blazor.reconnect(); |
||||||
|
if (!successful) { |
||||||
|
// We have been able to reach the server, but the circuit is no longer available.
|
||||||
|
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||||
|
const resumeSuccessful = await Blazor.resumeCircuit(); |
||||||
|
if (!resumeSuccessful) { |
||||||
|
location.reload(); |
||||||
|
} else { |
||||||
|
reconnectModal.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
// We got an exception, server is currently unavailable
|
||||||
|
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function resume() { |
||||||
|
try { |
||||||
|
const successful = await Blazor.resumeCircuit(); |
||||||
|
if (!successful) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
} catch { |
||||||
|
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function retryWhenDocumentBecomesVisible() { |
||||||
|
if (document.visibilityState === "visible") { |
||||||
|
await retry(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,640 @@ |
|||||||
|
@page "/can-monitor" |
||||||
|
@rendermode InteractiveServer |
||||||
|
@implements IDisposable |
||||||
|
|
||||||
|
@using Peak.Can.Basic |
||||||
|
|
||||||
|
@inject ICanService CanService |
||||||
|
|
||||||
|
<PageTitle>CAN Monitor</PageTitle> |
||||||
|
|
||||||
|
<h1>CAN Monitor</h1> |
||||||
|
|
||||||
|
@* ── Connection ──────────────────────────────────────────────────────────────── *@ |
||||||
|
<div class="card mb-3"> |
||||||
|
<div class="card-body py-2"> |
||||||
|
@if (CanService.IsConnected && !showConnectionForm) |
||||||
|
{ |
||||||
|
<div class="d-flex align-items-center gap-3"> |
||||||
|
<span class="badge bg-success">● Connected</span> |
||||||
|
<span class="font-monospace fw-semibold">@CanService.ChannelName</span> |
||||||
|
<span class="text-muted">@BitrateLabel(CanService.CurrentBitrate)</span> |
||||||
|
<button class="btn btn-sm btn-outline-secondary ms-auto" |
||||||
|
@onclick="() => { showConnectionForm = true; ScanChannels(); }"> |
||||||
|
Change |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
<div class="row g-2 align-items-end"> |
||||||
|
<div class="col-auto"> |
||||||
|
<label class="form-label small mb-0">Channel</label> |
||||||
|
<select class="form-select form-select-sm font-monospace" @bind="selectedChannel" |
||||||
|
style="min-width:100px"> |
||||||
|
@if (availableChannels.Count == 0) |
||||||
|
{ |
||||||
|
<option value="">— none found —</option> |
||||||
|
} |
||||||
|
@foreach (var ch in availableChannels) |
||||||
|
{ |
||||||
|
<option value="@ch.ToString()">@ch.ToString()</option> |
||||||
|
} |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<div class="col-auto"> |
||||||
|
<label class="form-label small mb-0">Bitrate</label> |
||||||
|
<select class="form-select form-select-sm" @bind="selectedBitrate" |
||||||
|
style="min-width:120px"> |
||||||
|
@foreach (var (val, label) in BitrateOptions) |
||||||
|
{ |
||||||
|
<option value="@val.ToString()">@label</option> |
||||||
|
} |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<div class="col-auto d-flex gap-2"> |
||||||
|
<button class="btn btn-sm btn-outline-secondary" @onclick="ScanChannels">Scan</button> |
||||||
|
<button class="btn btn-sm btn-primary" @onclick="Connect" |
||||||
|
disabled="@(availableChannels.Count == 0 || isConnecting)"> |
||||||
|
@(isConnecting ? "Connecting…" : CanService.IsConnected ? "Reconnect" : "Connect") |
||||||
|
</button> |
||||||
|
@if (CanService.IsConnected) |
||||||
|
{ |
||||||
|
<button class="btn btn-sm btn-outline-secondary" |
||||||
|
@onclick="() => showConnectionForm = false">Cancel</button> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
@if (availableChannels.Count == 0) |
||||||
|
{ |
||||||
|
<div class="text-warning small mt-1"> |
||||||
|
No PCAN USB channels detected. Plug in your adapter and click Scan. |
||||||
|
</div> |
||||||
|
} |
||||||
|
@if (connectError != null) |
||||||
|
{ |
||||||
|
<div class="text-danger small mt-1">@connectError</div> |
||||||
|
} |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@* ── Live messages ──────────────────────────────────────────────────────────── *@ |
||||||
|
<div class="card mb-3"> |
||||||
|
<div class="card-header d-flex justify-content-between align-items-center py-0 ps-0"> |
||||||
|
<ul class="nav nav-tabs border-0 mb-0"> |
||||||
|
<li class="nav-item"> |
||||||
|
<button class="nav-link rounded-0 @(msgTab == "stream" ? "active" : "")" |
||||||
|
@onclick='() => msgTab = "stream"'> |
||||||
|
Stream |
||||||
|
<span class="badge bg-secondary ms-1">@messages.Count</span> |
||||||
|
</button> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<button class="nav-link rounded-0 @(msgTab == "latest" ? "active" : "")" |
||||||
|
@onclick='() => msgTab = "latest"'> |
||||||
|
Latest |
||||||
|
<span class="badge bg-secondary ms-1">@latestMessages.Count</span> |
||||||
|
</button> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
<button class="btn btn-sm btn-outline-secondary me-2" @onclick="ClearMessages">Clear</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
@if (msgTab == "stream") |
||||||
|
{ |
||||||
|
<div style="max-height:320px; overflow-y:auto;"> |
||||||
|
<table class="table table-sm table-hover font-monospace mb-0"> |
||||||
|
<thead class="table-light sticky-top"> |
||||||
|
<tr> |
||||||
|
<th>Timestamp (µs)</th> |
||||||
|
<th>ID</th> |
||||||
|
<th>DLC</th> |
||||||
|
<th>Data</th> |
||||||
|
<th>Signals</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
@lock (messages) |
||||||
|
{ |
||||||
|
foreach (var m in messages) |
||||||
|
{ |
||||||
|
<tr> |
||||||
|
<td>@m.TimestampUs</td> |
||||||
|
<td>@($"0x{m.Id:X3}")</td> |
||||||
|
<td>@m.Dlc</td> |
||||||
|
<td>@(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–")</td> |
||||||
|
<td>@SignalsText(m)</td> |
||||||
|
</tr> |
||||||
|
} |
||||||
|
} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
<div style="max-height:320px; overflow-y:auto;"> |
||||||
|
<table class="table table-sm table-hover font-monospace mb-0"> |
||||||
|
<thead class="table-light sticky-top"> |
||||||
|
<tr> |
||||||
|
<th>ID</th> |
||||||
|
<th>DLC</th> |
||||||
|
<th>Data</th> |
||||||
|
<th>Signals</th> |
||||||
|
<th>Timestamp (µs)</th> |
||||||
|
<th class="text-end">Count</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
@foreach (var m in GetLatestRows()) |
||||||
|
{ |
||||||
|
<tr> |
||||||
|
<td>@($"0x{m.Id:X3}")</td> |
||||||
|
<td>@m.Dlc</td> |
||||||
|
<td>@(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–")</td> |
||||||
|
<td>@SignalsText(m)</td> |
||||||
|
<td>@m.TimestampUs</td> |
||||||
|
<td class="text-end">@updateCounts.GetValueOrDefault(m.Id)</td> |
||||||
|
</tr> |
||||||
|
} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
} |
||||||
|
</div> |
||||||
|
|
||||||
|
@* ── Filters + Send (two columns) ──────────────────────────────────────────── *@ |
||||||
|
<div class="row g-3 mb-3"> |
||||||
|
|
||||||
|
@* Filters *@ |
||||||
|
<div class="col-lg-7"> |
||||||
|
<div class="card h-100"> |
||||||
|
<div class="card-header"> |
||||||
|
Filters |
||||||
|
<span class="text-muted small ms-2"> |
||||||
|
@(filters.Count == 0 ? "(no filters — all messages pass)" : $"({filters.Count} active)") |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="card-body"> |
||||||
|
@if (filters.Count > 0) |
||||||
|
{ |
||||||
|
<table class="table table-sm mb-2"> |
||||||
|
<thead><tr><th>ID</th><th>Mask</th><th>Description</th><th></th></tr></thead> |
||||||
|
<tbody> |
||||||
|
@foreach (var f in filters) |
||||||
|
{ |
||||||
|
<tr> |
||||||
|
<td class="font-monospace">@($"0x{f.MessageId:X3}")</td> |
||||||
|
<td class="font-monospace">@($"0x{f.Mask:X3}")</td> |
||||||
|
<td>@f.Description</td> |
||||||
|
<td> |
||||||
|
<button class="btn btn-sm btn-outline-danger py-0 px-2" |
||||||
|
@onclick="() => RemoveFilter(f.Id)">×</button> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
} |
||||||
|
|
||||||
|
<div class="row g-1 align-items-end"> |
||||||
|
<div class="col-3"> |
||||||
|
<label class="form-label small mb-0">ID (hex)</label> |
||||||
|
<input class="form-control form-control-sm font-monospace" |
||||||
|
placeholder="e.g. 100" @bind="newFilterId" /> |
||||||
|
</div> |
||||||
|
<div class="col-3"> |
||||||
|
<label class="form-label small mb-0">Mask (hex)</label> |
||||||
|
<input class="form-control form-control-sm font-monospace" |
||||||
|
placeholder="e.g. 7FF" @bind="newFilterMask" /> |
||||||
|
</div> |
||||||
|
<div class="col-4"> |
||||||
|
<label class="form-label small mb-0">Description</label> |
||||||
|
<input class="form-control form-control-sm" |
||||||
|
placeholder="optional" @bind="newFilterDesc" /> |
||||||
|
</div> |
||||||
|
<div class="col-2"> |
||||||
|
<button class="btn btn-sm btn-primary w-100" @onclick="AddFilter">Add</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@if (filterError != null) |
||||||
|
{ |
||||||
|
<div class="text-danger small mt-1">@filterError</div> |
||||||
|
} |
||||||
|
@if (filters.Count > 0) |
||||||
|
{ |
||||||
|
<button class="btn btn-sm btn-outline-danger mt-2" @onclick="ClearFilters">Clear all</button> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@* Send *@ |
||||||
|
<div class="col-lg-5"> |
||||||
|
<div class="card h-100"> |
||||||
|
<div class="card-header">Send Message</div> |
||||||
|
<div class="card-body"> |
||||||
|
<div class="mb-2"> |
||||||
|
<label class="form-label small mb-0">CAN ID (hex)</label> |
||||||
|
<input class="form-control form-control-sm font-monospace" |
||||||
|
placeholder="e.g. 7FF" @bind="sendIdHex" /> |
||||||
|
</div> |
||||||
|
<div class="mb-2"> |
||||||
|
<label class="form-label small mb-0">Data bytes (hex, space-separated)</label> |
||||||
|
<input class="form-control form-control-sm font-monospace" |
||||||
|
placeholder="e.g. DE AD BE EF" @bind="sendDataHex" /> |
||||||
|
</div> |
||||||
|
<div class="form-check mb-2"> |
||||||
|
<input class="form-check-input" type="checkbox" id="extFrame" @bind="sendExtended" /> |
||||||
|
<label class="form-check-label small" for="extFrame">Extended frame (29-bit ID)</label> |
||||||
|
</div> |
||||||
|
<div class="d-flex align-items-center gap-2"> |
||||||
|
<button class="btn btn-sm btn-success" @onclick="SendMessage">Send</button> |
||||||
|
@if (sendFeedback != null) |
||||||
|
{ |
||||||
|
<span class="small @(sendOk ? "text-success" : "text-danger")">@sendFeedback</span> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@* ── Bitmasks ───────────────────────────────────────────────────────────────── *@ |
||||||
|
<div class="card"> |
||||||
|
<div class="card-header">Signal Bitmasks</div> |
||||||
|
<div class="card-body"> |
||||||
|
@if (bitmasks.Count == 0) |
||||||
|
{ |
||||||
|
<p class="text-muted small mb-2"> |
||||||
|
No bitmasks configured. Add one to extract named signal values from a message's data bytes.<br /> |
||||||
|
<em>Formula: physicalValue = ((rawUint64 & DataMask) >> RightShift) × Scale + Offset</em> |
||||||
|
</p> |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
<table class="table table-sm mb-2"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Msg ID</th><th>Signal</th><th>DataMask (64-bit)</th> |
||||||
|
<th>Shift</th><th>Scale</th><th>Offset</th><th>Unit</th><th></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
@foreach (var b in bitmasks) |
||||||
|
{ |
||||||
|
<tr> |
||||||
|
<td class="font-monospace">@($"0x{b.MessageId:X3}")</td> |
||||||
|
<td>@b.SignalName</td> |
||||||
|
<td class="font-monospace small">@($"0x{b.DataMask:X16}")</td> |
||||||
|
<td>@b.RightShift</td> |
||||||
|
<td>@b.Scale</td> |
||||||
|
<td>@b.Offset</td> |
||||||
|
<td>@b.Unit</td> |
||||||
|
<td> |
||||||
|
<button class="btn btn-sm btn-outline-danger py-0 px-2" |
||||||
|
@onclick="() => RemoveBitmask(b.Id)">×</button> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
} |
||||||
|
|
||||||
|
<div class="row g-1 align-items-end"> |
||||||
|
<div class="col-sm-2"> |
||||||
|
<label class="form-label small mb-0">Msg ID (hex)</label> |
||||||
|
<input class="form-control form-control-sm font-monospace" placeholder="e.g. 100" @bind="newBmMsgId" /> |
||||||
|
</div> |
||||||
|
<div class="col-sm-2"> |
||||||
|
<label class="form-label small mb-0">Signal name</label> |
||||||
|
<input class="form-control form-control-sm" placeholder="e.g. EngineRPM" @bind="newBmName" /> |
||||||
|
</div> |
||||||
|
<div class="col-sm-3"> |
||||||
|
<label class="form-label small mb-0">DataMask (64-bit hex)</label> |
||||||
|
<input class="form-control form-control-sm font-monospace" placeholder="e.g. 00FF000000000000" @bind="newBmMask" /> |
||||||
|
</div> |
||||||
|
<div class="col-sm-1"> |
||||||
|
<label class="form-label small mb-0">Shift</label> |
||||||
|
<input class="form-control form-control-sm" @bind="newBmShift" /> |
||||||
|
</div> |
||||||
|
<div class="col-sm-1"> |
||||||
|
<label class="form-label small mb-0">Scale</label> |
||||||
|
<input class="form-control form-control-sm" @bind="newBmScale" /> |
||||||
|
</div> |
||||||
|
<div class="col-sm-1"> |
||||||
|
<label class="form-label small mb-0">Offset</label> |
||||||
|
<input class="form-control form-control-sm" @bind="newBmOffset" /> |
||||||
|
</div> |
||||||
|
<div class="col-sm-1"> |
||||||
|
<label class="form-label small mb-0">Unit</label> |
||||||
|
<input class="form-control form-control-sm" placeholder="rpm" @bind="newBmUnit" /> |
||||||
|
</div> |
||||||
|
<div class="col-sm-1"> |
||||||
|
<button class="btn btn-sm btn-primary w-100" @onclick="AddBitmask">Add</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@if (bitmaskError != null) |
||||||
|
{ |
||||||
|
<div class="text-danger small mt-1">@bitmaskError</div> |
||||||
|
} |
||||||
|
@if (bitmasks.Count > 0) |
||||||
|
{ |
||||||
|
<button class="btn btn-sm btn-outline-danger mt-2" @onclick="ClearBitmasks">Clear all</button> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@code { |
||||||
|
// ── Connection form ─────────────────────────────────────────────────────── |
||||||
|
private List<PcanChannel> availableChannels = []; |
||||||
|
private string selectedChannel = ""; |
||||||
|
private string selectedBitrate = Bitrate.Pcan500.ToString(); |
||||||
|
private bool showConnectionForm; |
||||||
|
private bool isConnecting; |
||||||
|
private string? connectError; |
||||||
|
|
||||||
|
private static readonly (Bitrate Value, string Label)[] BitrateOptions = |
||||||
|
[ |
||||||
|
(Bitrate.Pcan1000, "1000 kbps"), |
||||||
|
(Bitrate.Pcan800, "800 kbps"), |
||||||
|
(Bitrate.Pcan500, "500 kbps"), |
||||||
|
(Bitrate.Pcan250, "250 kbps"), |
||||||
|
(Bitrate.Pcan125, "125 kbps"), |
||||||
|
(Bitrate.Pcan100, "100 kbps"), |
||||||
|
(Bitrate.Pcan50, "50 kbps"), |
||||||
|
(Bitrate.Pcan20, "20 kbps"), |
||||||
|
(Bitrate.Pcan10, "10 kbps"), |
||||||
|
(Bitrate.Pcan5, "5 kbps"), |
||||||
|
]; |
||||||
|
|
||||||
|
// ── Data ────────────────────────────────────────────────────────────────── |
||||||
|
private readonly List<CanMessageDto> messages = []; |
||||||
|
private readonly Dictionary<uint, CanMessageDto> latestMessages = new(); |
||||||
|
private readonly Dictionary<uint, int> updateCounts = new(); |
||||||
|
private string msgTab = "stream"; |
||||||
|
private const int MaxMessages = 200; |
||||||
|
private List<CanFilter> filters = []; |
||||||
|
private List<CanBitmask> bitmasks = []; |
||||||
|
|
||||||
|
// ── Filter form ─────────────────────────────────────────────────────────── |
||||||
|
private string newFilterId = ""; |
||||||
|
private string newFilterMask = "7FF"; |
||||||
|
private string newFilterDesc = ""; |
||||||
|
private string? filterError; |
||||||
|
|
||||||
|
// ── Bitmask form ────────────────────────────────────────────────────────── |
||||||
|
private string newBmMsgId = ""; |
||||||
|
private string newBmName = ""; |
||||||
|
private string newBmMask = ""; |
||||||
|
private int newBmShift = 0; |
||||||
|
private double newBmScale = 1.0; |
||||||
|
private double newBmOffset = 0.0; |
||||||
|
private string newBmUnit = ""; |
||||||
|
private string? bitmaskError; |
||||||
|
|
||||||
|
// ── Send form ───────────────────────────────────────────────────────────── |
||||||
|
private string sendIdHex = ""; |
||||||
|
private string sendDataHex = ""; |
||||||
|
private bool sendExtended; |
||||||
|
private string? sendFeedback; |
||||||
|
private bool sendOk; |
||||||
|
|
||||||
|
// ── Lifecycle ───────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
protected override void OnInitialized() |
||||||
|
{ |
||||||
|
filters = CanService.Filters.ToList(); |
||||||
|
bitmasks = CanService.Bitmasks.ToList(); |
||||||
|
CanService.MessageReceived += OnMessageReceived; |
||||||
|
|
||||||
|
ScanChannels(); |
||||||
|
if (CanService.IsConnected) |
||||||
|
{ |
||||||
|
selectedChannel = CanService.ChannelName; |
||||||
|
selectedBitrate = CanService.CurrentBitrate.ToString(); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
showConnectionForm = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void OnMessageReceived(CanMessageDto msg) |
||||||
|
{ |
||||||
|
lock (messages) |
||||||
|
{ |
||||||
|
messages.Insert(0, msg); |
||||||
|
if (messages.Count > MaxMessages) |
||||||
|
messages.RemoveAt(messages.Count - 1); |
||||||
|
} |
||||||
|
lock (latestMessages) |
||||||
|
{ |
||||||
|
if (!latestMessages.TryGetValue(msg.Id, out var existing) || |
||||||
|
!msg.Data.SequenceEqual(existing.Data)) |
||||||
|
{ |
||||||
|
latestMessages[msg.Id] = msg; |
||||||
|
updateCounts[msg.Id] = updateCounts.GetValueOrDefault(msg.Id) + 1; |
||||||
|
} |
||||||
|
} |
||||||
|
InvokeAsync(StateHasChanged); |
||||||
|
} |
||||||
|
|
||||||
|
public void Dispose() => CanService.MessageReceived -= OnMessageReceived; |
||||||
|
|
||||||
|
// ── Connection ──────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
private void ScanChannels() |
||||||
|
{ |
||||||
|
availableChannels = CanService.GetAvailableChannels().ToList(); |
||||||
|
if (availableChannels.Count > 0 && |
||||||
|
!availableChannels.Any(c => c.ToString() == selectedChannel)) |
||||||
|
selectedChannel = availableChannels[0].ToString(); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task Connect() |
||||||
|
{ |
||||||
|
connectError = null; |
||||||
|
if (!Enum.TryParse<PcanChannel>(selectedChannel, out var channel)) |
||||||
|
{ connectError = "Please select a channel."; return; } |
||||||
|
if (!Enum.TryParse<Bitrate>(selectedBitrate, out var bitrate)) |
||||||
|
{ connectError = "Please select a bitrate."; return; } |
||||||
|
|
||||||
|
isConnecting = true; |
||||||
|
CanService.RequestReinitialize(channel, bitrate); |
||||||
|
|
||||||
|
// Poll until the worker completes the reinit (typically < 50 ms) |
||||||
|
for (int i = 0; i < 20; i++) |
||||||
|
{ |
||||||
|
await Task.Delay(100); |
||||||
|
if (CanService.IsConnected) break; |
||||||
|
} |
||||||
|
|
||||||
|
isConnecting = false; |
||||||
|
if (CanService.IsConnected) |
||||||
|
showConnectionForm = false; |
||||||
|
else |
||||||
|
connectError = "Connection failed. Check the adapter is plugged in and try again."; |
||||||
|
|
||||||
|
StateHasChanged(); |
||||||
|
} |
||||||
|
|
||||||
|
private static string BitrateLabel(Bitrate b) => b switch |
||||||
|
{ |
||||||
|
Bitrate.Pcan1000 => "1000 kbps", |
||||||
|
Bitrate.Pcan800 => "800 kbps", |
||||||
|
Bitrate.Pcan500 => "500 kbps", |
||||||
|
Bitrate.Pcan250 => "250 kbps", |
||||||
|
Bitrate.Pcan125 => "125 kbps", |
||||||
|
Bitrate.Pcan100 => "100 kbps", |
||||||
|
Bitrate.Pcan50 => "50 kbps", |
||||||
|
Bitrate.Pcan20 => "20 kbps", |
||||||
|
Bitrate.Pcan10 => "10 kbps", |
||||||
|
Bitrate.Pcan5 => "5 kbps", |
||||||
|
_ => b.ToString() |
||||||
|
}; |
||||||
|
|
||||||
|
// ── Messages ────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
private void ClearMessages() |
||||||
|
{ |
||||||
|
lock (messages) messages.Clear(); |
||||||
|
lock (latestMessages) { latestMessages.Clear(); updateCounts.Clear(); } |
||||||
|
} |
||||||
|
|
||||||
|
// ── Filters ─────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
private void AddFilter() |
||||||
|
{ |
||||||
|
filterError = null; |
||||||
|
if (!TryParseHex32(newFilterId, out uint msgId)) |
||||||
|
{ filterError = "Invalid ID — enter a hex value e.g. 100"; return; } |
||||||
|
if (!TryParseHex32(newFilterMask, out uint mask)) |
||||||
|
{ filterError = "Invalid mask — enter a hex value e.g. 7FF"; return; } |
||||||
|
|
||||||
|
var filter = new CanFilter { MessageId = msgId, Mask = mask, Description = newFilterDesc }; |
||||||
|
CanService.AddFilter(filter); |
||||||
|
filters = CanService.Filters.ToList(); |
||||||
|
newFilterId = ""; newFilterMask = "7FF"; newFilterDesc = ""; |
||||||
|
} |
||||||
|
|
||||||
|
private void RemoveFilter(Guid id) |
||||||
|
{ |
||||||
|
CanService.RemoveFilter(id); |
||||||
|
filters = CanService.Filters.ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
private void ClearFilters() |
||||||
|
{ |
||||||
|
CanService.ClearFilters(); |
||||||
|
filters = []; |
||||||
|
} |
||||||
|
|
||||||
|
// ── Bitmasks ────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
private void AddBitmask() |
||||||
|
{ |
||||||
|
bitmaskError = null; |
||||||
|
if (!TryParseHex32(newBmMsgId, out uint msgId)) |
||||||
|
{ bitmaskError = "Invalid Msg ID"; return; } |
||||||
|
if (string.IsNullOrWhiteSpace(newBmName)) |
||||||
|
{ bitmaskError = "Signal name is required"; return; } |
||||||
|
if (!TryParseHex64(newBmMask, out ulong mask)) |
||||||
|
{ bitmaskError = "Invalid 64-bit mask — enter hex e.g. 00FF000000000000"; return; } |
||||||
|
|
||||||
|
var bitmask = new CanBitmask |
||||||
|
{ |
||||||
|
MessageId = msgId, |
||||||
|
SignalName = newBmName.Trim(), |
||||||
|
DataMask = mask, |
||||||
|
RightShift = newBmShift, |
||||||
|
Scale = newBmScale, |
||||||
|
Offset = newBmOffset, |
||||||
|
Unit = string.IsNullOrWhiteSpace(newBmUnit) ? null : newBmUnit |
||||||
|
}; |
||||||
|
CanService.AddBitmask(bitmask); |
||||||
|
bitmasks = CanService.Bitmasks.ToList(); |
||||||
|
newBmMsgId = ""; newBmName = ""; newBmMask = ""; |
||||||
|
newBmShift = 0; newBmScale = 1.0; newBmOffset = 0.0; newBmUnit = ""; |
||||||
|
} |
||||||
|
|
||||||
|
private void RemoveBitmask(Guid id) |
||||||
|
{ |
||||||
|
CanService.RemoveBitmask(id); |
||||||
|
bitmasks = CanService.Bitmasks.ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
private void ClearBitmasks() |
||||||
|
{ |
||||||
|
CanService.ClearBitmasks(); |
||||||
|
bitmasks = []; |
||||||
|
} |
||||||
|
|
||||||
|
// ── Send ────────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
private void SendMessage() |
||||||
|
{ |
||||||
|
sendFeedback = null; |
||||||
|
if (!TryParseHex32(sendIdHex, out uint id)) |
||||||
|
{ sendFeedback = "Invalid ID"; sendOk = false; return; } |
||||||
|
|
||||||
|
byte[] data; |
||||||
|
try |
||||||
|
{ |
||||||
|
var hex = (sendDataHex ?? "").Replace(" ", ""); |
||||||
|
data = hex.Length > 0 ? Convert.FromHexString(hex) : []; |
||||||
|
} |
||||||
|
catch { sendFeedback = "Invalid hex data"; sendOk = false; return; } |
||||||
|
|
||||||
|
if (data.Length > 8) |
||||||
|
{ sendFeedback = "Max 8 bytes"; sendOk = false; return; } |
||||||
|
|
||||||
|
var msg = new PcanMessage |
||||||
|
{ |
||||||
|
ID = id, |
||||||
|
MsgType = sendExtended ? MessageType.Extended : MessageType.Standard, |
||||||
|
DLC = (byte)data.Length, |
||||||
|
Data = new byte[8] |
||||||
|
}; |
||||||
|
data.CopyTo(msg.Data, 0); |
||||||
|
|
||||||
|
var result = CanService.Write(msg); |
||||||
|
sendOk = result == PcanStatus.OK; |
||||||
|
sendFeedback = sendOk ? "Sent!" : $"Error: {result}"; |
||||||
|
} |
||||||
|
|
||||||
|
// ── Helpers ─────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
private List<CanMessageDto> GetLatestRows() |
||||||
|
{ |
||||||
|
lock (latestMessages) |
||||||
|
return latestMessages.Values.OrderBy(m => m.Id).ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
private static string SignalsText(CanMessageDto m) |
||||||
|
{ |
||||||
|
if (m.Signals is null || m.Signals.Count == 0) return "–"; |
||||||
|
return string.Join(", ", m.Signals.Select(kv => $"{kv.Key}={kv.Value:G4}")); |
||||||
|
} |
||||||
|
|
||||||
|
private static bool TryParseHex32(string? s, out uint value) |
||||||
|
{ |
||||||
|
value = 0; |
||||||
|
if (string.IsNullOrWhiteSpace(s)) return false; |
||||||
|
var hex = s.Trim(); |
||||||
|
if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..]; |
||||||
|
return uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value); |
||||||
|
} |
||||||
|
|
||||||
|
private static bool TryParseHex64(string? s, out ulong value) |
||||||
|
{ |
||||||
|
value = 0; |
||||||
|
if (string.IsNullOrWhiteSpace(s)) return false; |
||||||
|
var hex = s.Trim(); |
||||||
|
if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..]; |
||||||
|
return ulong.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
@page "/scott" |
||||||
|
|
||||||
|
<PageTitle>Scott</PageTitle> |
||||||
|
|
||||||
|
<div class="scott-wrapper"> |
||||||
|
<div class="poo-orbit"> |
||||||
|
<div class="poo-spin"> |
||||||
|
<div class="poo-wobble"> |
||||||
|
<div class="poo-pulse"> |
||||||
|
<span class="poo-text">💩POO💩</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.scott-wrapper { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
height: 70vh; |
||||||
|
overflow: hidden; |
||||||
|
background: radial-gradient(ellipse at center, #1a0033 0%, #000 100%); |
||||||
|
border-radius: 12px; |
||||||
|
margin-top: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
.poo-orbit { |
||||||
|
animation: orbit 3s linear infinite; |
||||||
|
} |
||||||
|
|
||||||
|
.poo-spin { |
||||||
|
animation: spin 0.4s linear infinite; |
||||||
|
} |
||||||
|
|
||||||
|
.poo-wobble { |
||||||
|
animation: wobble 0.7s ease-in-out infinite alternate; |
||||||
|
} |
||||||
|
|
||||||
|
.poo-pulse { |
||||||
|
animation: pulse 0.5s ease-in-out infinite alternate; |
||||||
|
} |
||||||
|
|
||||||
|
.poo-text { |
||||||
|
font-size: clamp(4rem, 12vw, 9rem); |
||||||
|
font-weight: 900; |
||||||
|
font-family: Impact, sans-serif; |
||||||
|
letter-spacing: 0.05em; |
||||||
|
background: linear-gradient( |
||||||
|
90deg, |
||||||
|
#ff0000, #ff7700, #ffff00, |
||||||
|
#00ff00, #0000ff, #8b00ff, |
||||||
|
#ff0000 |
||||||
|
); |
||||||
|
background-size: 300% auto; |
||||||
|
-webkit-background-clip: text; |
||||||
|
-webkit-text-fill-color: transparent; |
||||||
|
background-clip: text; |
||||||
|
animation: rainbow 0.8s linear infinite; |
||||||
|
filter: drop-shadow(0 0 18px #ff00ff) drop-shadow(0 0 40px #00ffff); |
||||||
|
display: inline-block; |
||||||
|
text-shadow: none; |
||||||
|
} |
||||||
|
|
||||||
|
@@keyframes orbit { |
||||||
|
0% { transform: rotate(0deg) translateX(40px) rotate(0deg); } |
||||||
|
100% { transform: rotate(360deg) translateX(40px) rotate(-360deg); } |
||||||
|
} |
||||||
|
|
||||||
|
@@keyframes spin { |
||||||
|
from { transform: rotate(0deg); } |
||||||
|
to { transform: rotate(360deg); } |
||||||
|
} |
||||||
|
|
||||||
|
@@keyframes wobble { |
||||||
|
from { transform: skewX(-15deg) scaleY(0.9); } |
||||||
|
to { transform: skewX(15deg) scaleY(1.15); } |
||||||
|
} |
||||||
|
|
||||||
|
@@keyframes pulse { |
||||||
|
from { transform: scale(0.85); } |
||||||
|
to { transform: scale(1.2); } |
||||||
|
} |
||||||
|
|
||||||
|
@@keyframes rainbow { |
||||||
|
0% { background-position: 0% center; } |
||||||
|
100% { background-position: 300% center; } |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||||
|
|
||||||
|
<PropertyGroup> |
||||||
|
<TargetFramework>net10.0</TargetFramework> |
||||||
|
<Nullable>enable</Nullable> |
||||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||||
|
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException> |
||||||
|
</PropertyGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.4" /> |
||||||
|
<PackageReference Include="Peak.PCANBasic.NET" Version="4.10.1.968" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
</Project> |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
namespace IOModuleTestBlazor.Models; |
||||||
|
|
||||||
|
public record CanMessageDto( |
||||||
|
uint Id, |
||||||
|
byte[] Data, |
||||||
|
int Dlc, |
||||||
|
ulong TimestampUs, |
||||||
|
bool IsExtended, |
||||||
|
IReadOnlyDictionary<string, double>? Signals |
||||||
|
); |
||||||
|
|
||||||
|
public class CanFilter |
||||||
|
{ |
||||||
|
public Guid Id { get; set; } |
||||||
|
public uint MessageId { get; set; } |
||||||
|
public uint Mask { get; set; } = 0x7FF; |
||||||
|
public string? Description { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class CanBitmask |
||||||
|
{ |
||||||
|
public Guid Id { get; set; } |
||||||
|
public uint MessageId { get; set; } |
||||||
|
public string SignalName { get; set; } = ""; |
||||||
|
public ulong DataMask { get; set; } |
||||||
|
public int RightShift { get; set; } |
||||||
|
public double Scale { get; set; } = 1.0; |
||||||
|
public double Offset { get; set; } |
||||||
|
public string? Unit { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class CanStatus |
||||||
|
{ |
||||||
|
public bool IsConnected { get; set; } |
||||||
|
public string ChannelName { get; set; } = ""; |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
using IOModuleTestBlazor; |
||||||
|
using IOModuleTestBlazor.Components; |
||||||
|
using IOModuleTestBlazor.Services; |
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args); |
||||||
|
|
||||||
|
// ── CAN bus ─────────────────────────────────────────────────────────────────── |
||||||
|
builder.Services.AddSingleton<ICanService, CanService>(); |
||||||
|
builder.Services.AddHostedService<CanWorker>(); |
||||||
|
|
||||||
|
// ── Blazor ──────────────────────────────────────────────────────────────────── |
||||||
|
builder.Services.AddRazorComponents() |
||||||
|
.AddInteractiveServerComponents(); |
||||||
|
|
||||||
|
var app = builder.Build(); |
||||||
|
|
||||||
|
if (!app.Environment.IsDevelopment()) |
||||||
|
{ |
||||||
|
app.UseExceptionHandler("/Error", createScopeForErrors: true); |
||||||
|
app.UseHsts(); |
||||||
|
} |
||||||
|
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); |
||||||
|
app.UseHttpsRedirection(); |
||||||
|
|
||||||
|
app.UseAntiforgery(); |
||||||
|
|
||||||
|
app.MapStaticAssets(); |
||||||
|
app.MapRazorComponents<App>() |
||||||
|
.AddInteractiveServerRenderMode(); |
||||||
|
|
||||||
|
app.Run(); |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
{ |
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json", |
||||||
|
"profiles": { |
||||||
|
"http": { |
||||||
|
"commandName": "Project", |
||||||
|
"dotnetRunMessages": true, |
||||||
|
"launchBrowser": true, |
||||||
|
"applicationUrl": "http://localhost:5042", |
||||||
|
"environmentVariables": { |
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||||
|
} |
||||||
|
}, |
||||||
|
"https": { |
||||||
|
"commandName": "Project", |
||||||
|
"dotnetRunMessages": true, |
||||||
|
"launchBrowser": true, |
||||||
|
"applicationUrl": "https://localhost:7074;http://localhost:5042", |
||||||
|
"environmentVariables": { |
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,172 @@ |
|||||||
|
using Peak.Can.Basic; |
||||||
|
using IOModuleTestBlazor.Models; |
||||||
|
|
||||||
|
namespace IOModuleTestBlazor.Services; |
||||||
|
|
||||||
|
public class CanService : ICanService |
||||||
|
{ |
||||||
|
private PcanChannel _channel; |
||||||
|
private readonly List<CanFilter> _filters = []; |
||||||
|
private readonly List<CanBitmask> _bitmasks = []; |
||||||
|
private readonly object _lock = new(); |
||||||
|
|
||||||
|
public bool IsConnected { get; private set; } |
||||||
|
public string ChannelName => IsConnected ? _channel.ToString() : ""; |
||||||
|
public Bitrate CurrentBitrate { get; private set; } |
||||||
|
|
||||||
|
private (PcanChannel Channel, Bitrate Bitrate)? _pendingReinit; |
||||||
|
|
||||||
|
public event Action<CanMessageDto>? MessageReceived; |
||||||
|
|
||||||
|
public void PublishMessage(CanMessageDto dto) => MessageReceived?.Invoke(dto); |
||||||
|
|
||||||
|
// ── Filters ─────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
public IReadOnlyList<CanFilter> Filters |
||||||
|
{ |
||||||
|
get { lock (_lock) return _filters.ToList(); } |
||||||
|
} |
||||||
|
|
||||||
|
public CanFilter AddFilter(CanFilter filter) |
||||||
|
{ |
||||||
|
lock (_lock) _filters.Add(filter); |
||||||
|
return filter; |
||||||
|
} |
||||||
|
|
||||||
|
public bool RemoveFilter(Guid id) |
||||||
|
{ |
||||||
|
lock (_lock) return _filters.RemoveAll(f => f.Id == id) > 0; |
||||||
|
} |
||||||
|
|
||||||
|
public void ClearFilters() |
||||||
|
{ |
||||||
|
lock (_lock) _filters.Clear(); |
||||||
|
} |
||||||
|
|
||||||
|
// ── Bitmasks ────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
public IReadOnlyList<CanBitmask> Bitmasks |
||||||
|
{ |
||||||
|
get { lock (_lock) return _bitmasks.ToList(); } |
||||||
|
} |
||||||
|
|
||||||
|
public CanBitmask AddBitmask(CanBitmask bitmask) |
||||||
|
{ |
||||||
|
lock (_lock) _bitmasks.Add(bitmask); |
||||||
|
return bitmask; |
||||||
|
} |
||||||
|
|
||||||
|
public bool RemoveBitmask(Guid id) |
||||||
|
{ |
||||||
|
lock (_lock) return _bitmasks.RemoveAll(b => b.Id == id) > 0; |
||||||
|
} |
||||||
|
|
||||||
|
public void ClearBitmasks() |
||||||
|
{ |
||||||
|
lock (_lock) _bitmasks.Clear(); |
||||||
|
} |
||||||
|
|
||||||
|
// ── Helpers ─────────────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
public bool PassesFilter(uint messageId) |
||||||
|
{ |
||||||
|
lock (_lock) |
||||||
|
{ |
||||||
|
if (_filters.Count == 0) return true; |
||||||
|
return _filters.Any(f => (messageId & f.Mask) == (f.MessageId & f.Mask)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, double> ExtractSignals(uint messageId, byte[] data) |
||||||
|
{ |
||||||
|
List<CanBitmask> applicable; |
||||||
|
lock (_lock) |
||||||
|
applicable = _bitmasks.Where(b => b.MessageId == messageId).ToList(); |
||||||
|
|
||||||
|
if (applicable.Count == 0) |
||||||
|
return new Dictionary<string, double>(); |
||||||
|
|
||||||
|
Span<byte> padded = stackalloc byte[8]; |
||||||
|
var len = Math.Min(data.Length, 8); |
||||||
|
for (int i = 0; i < len; i++) |
||||||
|
padded[i] = data[i]; |
||||||
|
|
||||||
|
ulong raw = 0; |
||||||
|
for (int i = 0; i < 8; i++) |
||||||
|
raw = (raw << 8) | padded[i]; |
||||||
|
|
||||||
|
var signals = new Dictionary<string, double>(applicable.Count); |
||||||
|
foreach (var bm in applicable) |
||||||
|
{ |
||||||
|
ulong masked = (raw & bm.DataMask) >> bm.RightShift; |
||||||
|
signals[bm.SignalName] = masked * bm.Scale + bm.Offset; |
||||||
|
} |
||||||
|
return signals; |
||||||
|
} |
||||||
|
|
||||||
|
// ── PCAN passthrough ────────────────────────────────────────────────────── |
||||||
|
|
||||||
|
public IReadOnlyList<PcanChannel> GetAvailableChannels() |
||||||
|
{ |
||||||
|
var available = new List<PcanChannel>(); |
||||||
|
foreach (var ch in Enum.GetValues<PcanChannel>() |
||||||
|
.Where(c => c.ToString().StartsWith("Usb", StringComparison.OrdinalIgnoreCase)) |
||||||
|
.OrderBy(c => c.ToString())) |
||||||
|
{ |
||||||
|
var result = Api.GetValue(ch, PcanParameter.ChannelCondition, out uint condition); |
||||||
|
if (result == PcanStatus.OK && condition != 0) |
||||||
|
available.Add(ch); |
||||||
|
} |
||||||
|
return available; |
||||||
|
} |
||||||
|
|
||||||
|
public void RequestReinitialize(PcanChannel channel, Bitrate bitrate) |
||||||
|
{ |
||||||
|
lock (_lock) _pendingReinit = (channel, bitrate); |
||||||
|
} |
||||||
|
|
||||||
|
public bool TryConsumePendingReinit(out PcanChannel channel, out Bitrate bitrate) |
||||||
|
{ |
||||||
|
lock (_lock) |
||||||
|
{ |
||||||
|
if (_pendingReinit.HasValue) |
||||||
|
{ |
||||||
|
channel = _pendingReinit.Value.Channel; |
||||||
|
bitrate = _pendingReinit.Value.Bitrate; |
||||||
|
_pendingReinit = null; |
||||||
|
return true; |
||||||
|
} |
||||||
|
channel = default; |
||||||
|
bitrate = default; |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void Initialize(PcanChannel channel, Bitrate bitrate) |
||||||
|
{ |
||||||
|
_channel = channel; |
||||||
|
var result = Api.Initialize(channel, bitrate); |
||||||
|
if (result != PcanStatus.OK) |
||||||
|
throw new InvalidOperationException($"CAN init failed: {GetErrorText(result)}"); |
||||||
|
IsConnected = true; |
||||||
|
CurrentBitrate = bitrate; |
||||||
|
} |
||||||
|
|
||||||
|
public void Uninitialize() |
||||||
|
{ |
||||||
|
Api.Uninitialize(_channel); |
||||||
|
IsConnected = false; |
||||||
|
} |
||||||
|
|
||||||
|
public PcanStatus Read(out PcanMessage msg, out ulong timestamp) |
||||||
|
=> Api.Read(_channel, out msg, out timestamp); |
||||||
|
|
||||||
|
public PcanStatus Write(PcanMessage msg) |
||||||
|
=> Api.Write(_channel, msg); |
||||||
|
|
||||||
|
private static string GetErrorText(PcanStatus status) |
||||||
|
{ |
||||||
|
try { Api.GetErrorText(status, out var text); return text; } |
||||||
|
catch (PcanBasicException) { return status.ToString(); } |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
using Peak.Can.Basic; |
||||||
|
using IOModuleTestBlazor.Models; |
||||||
|
|
||||||
|
namespace IOModuleTestBlazor.Services; |
||||||
|
|
||||||
|
public interface ICanService |
||||||
|
{ |
||||||
|
bool IsConnected { get; } |
||||||
|
string ChannelName { get; } |
||||||
|
Bitrate CurrentBitrate { get; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Raised on the worker thread each time a CAN message passes all filters. |
||||||
|
/// Subscribers must marshal UI updates with InvokeAsync(StateHasChanged). |
||||||
|
/// </summary> |
||||||
|
event Action<CanMessageDto> MessageReceived; |
||||||
|
|
||||||
|
// ── Filters ─────────────────────────────────────────────────────────────── |
||||||
|
IReadOnlyList<CanFilter> Filters { get; } |
||||||
|
CanFilter AddFilter(CanFilter filter); |
||||||
|
bool RemoveFilter(Guid id); |
||||||
|
void ClearFilters(); |
||||||
|
|
||||||
|
// ── Bitmasks ────────────────────────────────────────────────────────────── |
||||||
|
IReadOnlyList<CanBitmask> Bitmasks { get; } |
||||||
|
CanBitmask AddBitmask(CanBitmask bitmask); |
||||||
|
bool RemoveBitmask(Guid id); |
||||||
|
void ClearBitmasks(); |
||||||
|
|
||||||
|
// ── Helpers used by the worker ──────────────────────────────────────────── |
||||||
|
bool PassesFilter(uint messageId); |
||||||
|
IReadOnlyDictionary<string, double> ExtractSignals(uint messageId, byte[] data); |
||||||
|
void PublishMessage(CanMessageDto dto); |
||||||
|
|
||||||
|
// ── Channel management ──────────────────────────────────────────────────── |
||||||
|
|
||||||
|
/// <summary>Returns all PCAN USB channels that are currently available.</summary> |
||||||
|
IReadOnlyList<PcanChannel> GetAvailableChannels(); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Asks the worker to reinitialise on the next loop iteration. |
||||||
|
/// Non-blocking — the actual reconnect happens asynchronously. |
||||||
|
/// </summary> |
||||||
|
void RequestReinitialize(PcanChannel channel, Bitrate bitrate); |
||||||
|
|
||||||
|
/// <summary>Called by the worker to pick up a pending reinit request.</summary> |
||||||
|
bool TryConsumePendingReinit(out PcanChannel channel, out Bitrate bitrate); |
||||||
|
|
||||||
|
// ── PCAN passthrough ────────────────────────────────────────────────────── |
||||||
|
void Initialize(PcanChannel channel, Bitrate bitrate); |
||||||
|
void Uninitialize(); |
||||||
|
PcanStatus Read(out PcanMessage msg, out ulong timestamp); |
||||||
|
PcanStatus Write(PcanMessage msg); |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"Logging": { |
||||||
|
"LogLevel": { |
||||||
|
"Default": "Information", |
||||||
|
"Microsoft.AspNetCore": "Warning" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"Logging": { |
||||||
|
"LogLevel": { |
||||||
|
"Default": "Information", |
||||||
|
"Microsoft.AspNetCore": "Warning", |
||||||
|
"IOModuleTestBlazor.CanWorker": "Debug" |
||||||
|
} |
||||||
|
}, |
||||||
|
"AllowedHosts": "*", |
||||||
|
"CanOptions": { |
||||||
|
"Channel": "", |
||||||
|
"Bitrate": "Pcan500", |
||||||
|
"Filters": [], |
||||||
|
"Bitmasks": [] |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
html, body { |
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; |
||||||
|
} |
||||||
|
|
||||||
|
a, .btn-link { |
||||||
|
color: #006bb7; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-primary { |
||||||
|
color: #fff; |
||||||
|
background-color: #1b6ec2; |
||||||
|
border-color: #1861ac; |
||||||
|
} |
||||||
|
|
||||||
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { |
||||||
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; |
||||||
|
} |
||||||
|
|
||||||
|
.content { |
||||||
|
padding-top: 1.1rem; |
||||||
|
} |
||||||
|
|
||||||
|
h1:focus { |
||||||
|
outline: none; |
||||||
|
} |
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) { |
||||||
|
outline: 1px solid #26b050; |
||||||
|
} |
||||||
|
|
||||||
|
.invalid { |
||||||
|
outline: 1px solid #e50000; |
||||||
|
} |
||||||
|
|
||||||
|
.validation-message { |
||||||
|
color: #e50000; |
||||||
|
} |
||||||
|
|
||||||
|
.blazor-error-boundary { |
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; |
||||||
|
padding: 1rem 1rem 1rem 3.7rem; |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.blazor-error-boundary::after { |
||||||
|
content: "An error has occurred." |
||||||
|
} |
||||||
|
|
||||||
|
.darker-border-checkbox.form-check-input { |
||||||
|
border-color: #929292; |
||||||
|
} |
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { |
||||||
|
color: var(--bs-secondary-color); |
||||||
|
text-align: end; |
||||||
|
} |
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { |
||||||
|
text-align: start; |
||||||
|
} |
||||||
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,597 @@ |
|||||||
|
/*! |
||||||
|
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) |
||||||
|
* Copyright 2011-2024 The Bootstrap Authors |
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||||
|
*/ |
||||||
|
:root, |
||||||
|
[data-bs-theme=light] { |
||||||
|
--bs-blue: #0d6efd; |
||||||
|
--bs-indigo: #6610f2; |
||||||
|
--bs-purple: #6f42c1; |
||||||
|
--bs-pink: #d63384; |
||||||
|
--bs-red: #dc3545; |
||||||
|
--bs-orange: #fd7e14; |
||||||
|
--bs-yellow: #ffc107; |
||||||
|
--bs-green: #198754; |
||||||
|
--bs-teal: #20c997; |
||||||
|
--bs-cyan: #0dcaf0; |
||||||
|
--bs-black: #000; |
||||||
|
--bs-white: #fff; |
||||||
|
--bs-gray: #6c757d; |
||||||
|
--bs-gray-dark: #343a40; |
||||||
|
--bs-gray-100: #f8f9fa; |
||||||
|
--bs-gray-200: #e9ecef; |
||||||
|
--bs-gray-300: #dee2e6; |
||||||
|
--bs-gray-400: #ced4da; |
||||||
|
--bs-gray-500: #adb5bd; |
||||||
|
--bs-gray-600: #6c757d; |
||||||
|
--bs-gray-700: #495057; |
||||||
|
--bs-gray-800: #343a40; |
||||||
|
--bs-gray-900: #212529; |
||||||
|
--bs-primary: #0d6efd; |
||||||
|
--bs-secondary: #6c757d; |
||||||
|
--bs-success: #198754; |
||||||
|
--bs-info: #0dcaf0; |
||||||
|
--bs-warning: #ffc107; |
||||||
|
--bs-danger: #dc3545; |
||||||
|
--bs-light: #f8f9fa; |
||||||
|
--bs-dark: #212529; |
||||||
|
--bs-primary-rgb: 13, 110, 253; |
||||||
|
--bs-secondary-rgb: 108, 117, 125; |
||||||
|
--bs-success-rgb: 25, 135, 84; |
||||||
|
--bs-info-rgb: 13, 202, 240; |
||||||
|
--bs-warning-rgb: 255, 193, 7; |
||||||
|
--bs-danger-rgb: 220, 53, 69; |
||||||
|
--bs-light-rgb: 248, 249, 250; |
||||||
|
--bs-dark-rgb: 33, 37, 41; |
||||||
|
--bs-primary-text-emphasis: #052c65; |
||||||
|
--bs-secondary-text-emphasis: #2b2f32; |
||||||
|
--bs-success-text-emphasis: #0a3622; |
||||||
|
--bs-info-text-emphasis: #055160; |
||||||
|
--bs-warning-text-emphasis: #664d03; |
||||||
|
--bs-danger-text-emphasis: #58151c; |
||||||
|
--bs-light-text-emphasis: #495057; |
||||||
|
--bs-dark-text-emphasis: #495057; |
||||||
|
--bs-primary-bg-subtle: #cfe2ff; |
||||||
|
--bs-secondary-bg-subtle: #e2e3e5; |
||||||
|
--bs-success-bg-subtle: #d1e7dd; |
||||||
|
--bs-info-bg-subtle: #cff4fc; |
||||||
|
--bs-warning-bg-subtle: #fff3cd; |
||||||
|
--bs-danger-bg-subtle: #f8d7da; |
||||||
|
--bs-light-bg-subtle: #fcfcfd; |
||||||
|
--bs-dark-bg-subtle: #ced4da; |
||||||
|
--bs-primary-border-subtle: #9ec5fe; |
||||||
|
--bs-secondary-border-subtle: #c4c8cb; |
||||||
|
--bs-success-border-subtle: #a3cfbb; |
||||||
|
--bs-info-border-subtle: #9eeaf9; |
||||||
|
--bs-warning-border-subtle: #ffe69c; |
||||||
|
--bs-danger-border-subtle: #f1aeb5; |
||||||
|
--bs-light-border-subtle: #e9ecef; |
||||||
|
--bs-dark-border-subtle: #adb5bd; |
||||||
|
--bs-white-rgb: 255, 255, 255; |
||||||
|
--bs-black-rgb: 0, 0, 0; |
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); |
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif); |
||||||
|
--bs-body-font-size: 1rem; |
||||||
|
--bs-body-font-weight: 400; |
||||||
|
--bs-body-line-height: 1.5; |
||||||
|
--bs-body-color: #212529; |
||||||
|
--bs-body-color-rgb: 33, 37, 41; |
||||||
|
--bs-body-bg: #fff; |
||||||
|
--bs-body-bg-rgb: 255, 255, 255; |
||||||
|
--bs-emphasis-color: #000; |
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0; |
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75); |
||||||
|
--bs-secondary-color-rgb: 33, 37, 41; |
||||||
|
--bs-secondary-bg: #e9ecef; |
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239; |
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41; |
||||||
|
--bs-tertiary-bg: #f8f9fa; |
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #0d6efd; |
||||||
|
--bs-link-color-rgb: 13, 110, 253; |
||||||
|
--bs-link-decoration: underline; |
||||||
|
--bs-link-hover-color: #0a58ca; |
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202; |
||||||
|
--bs-code-color: #d63384; |
||||||
|
--bs-highlight-color: #212529; |
||||||
|
--bs-highlight-bg: #fff3cd; |
||||||
|
--bs-border-width: 1px; |
||||||
|
--bs-border-style: solid; |
||||||
|
--bs-border-color: #dee2e6; |
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); |
||||||
|
--bs-border-radius: 0.375rem; |
||||||
|
--bs-border-radius-sm: 0.25rem; |
||||||
|
--bs-border-radius-lg: 0.5rem; |
||||||
|
--bs-border-radius-xl: 1rem; |
||||||
|
--bs-border-radius-xxl: 2rem; |
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl); |
||||||
|
--bs-border-radius-pill: 50rem; |
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); |
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); |
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); |
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); |
||||||
|
--bs-focus-ring-width: 0.25rem; |
||||||
|
--bs-focus-ring-opacity: 0.25; |
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); |
||||||
|
--bs-form-valid-color: #198754; |
||||||
|
--bs-form-valid-border-color: #198754; |
||||||
|
--bs-form-invalid-color: #dc3545; |
||||||
|
--bs-form-invalid-border-color: #dc3545; |
||||||
|
} |
||||||
|
|
||||||
|
[data-bs-theme=dark] { |
||||||
|
color-scheme: dark; |
||||||
|
--bs-body-color: #dee2e6; |
||||||
|
--bs-body-color-rgb: 222, 226, 230; |
||||||
|
--bs-body-bg: #212529; |
||||||
|
--bs-body-bg-rgb: 33, 37, 41; |
||||||
|
--bs-emphasis-color: #fff; |
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255; |
||||||
|
--bs-secondary-color: rgba(222, 226, 230, 0.75); |
||||||
|
--bs-secondary-color-rgb: 222, 226, 230; |
||||||
|
--bs-secondary-bg: #343a40; |
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64; |
||||||
|
--bs-tertiary-color: rgba(222, 226, 230, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 222, 226, 230; |
||||||
|
--bs-tertiary-bg: #2b3035; |
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53; |
||||||
|
--bs-primary-text-emphasis: #6ea8fe; |
||||||
|
--bs-secondary-text-emphasis: #a7acb1; |
||||||
|
--bs-success-text-emphasis: #75b798; |
||||||
|
--bs-info-text-emphasis: #6edff6; |
||||||
|
--bs-warning-text-emphasis: #ffda6a; |
||||||
|
--bs-danger-text-emphasis: #ea868f; |
||||||
|
--bs-light-text-emphasis: #f8f9fa; |
||||||
|
--bs-dark-text-emphasis: #dee2e6; |
||||||
|
--bs-primary-bg-subtle: #031633; |
||||||
|
--bs-secondary-bg-subtle: #161719; |
||||||
|
--bs-success-bg-subtle: #051b11; |
||||||
|
--bs-info-bg-subtle: #032830; |
||||||
|
--bs-warning-bg-subtle: #332701; |
||||||
|
--bs-danger-bg-subtle: #2c0b0e; |
||||||
|
--bs-light-bg-subtle: #343a40; |
||||||
|
--bs-dark-bg-subtle: #1a1d20; |
||||||
|
--bs-primary-border-subtle: #084298; |
||||||
|
--bs-secondary-border-subtle: #41464b; |
||||||
|
--bs-success-border-subtle: #0f5132; |
||||||
|
--bs-info-border-subtle: #087990; |
||||||
|
--bs-warning-border-subtle: #997404; |
||||||
|
--bs-danger-border-subtle: #842029; |
||||||
|
--bs-light-border-subtle: #495057; |
||||||
|
--bs-dark-border-subtle: #343a40; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #6ea8fe; |
||||||
|
--bs-link-hover-color: #8bb9fe; |
||||||
|
--bs-link-color-rgb: 110, 168, 254; |
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254; |
||||||
|
--bs-code-color: #e685b5; |
||||||
|
--bs-highlight-color: #dee2e6; |
||||||
|
--bs-highlight-bg: #664d03; |
||||||
|
--bs-border-color: #495057; |
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); |
||||||
|
--bs-form-valid-color: #75b798; |
||||||
|
--bs-form-valid-border-color: #75b798; |
||||||
|
--bs-form-invalid-color: #ea868f; |
||||||
|
--bs-form-invalid-border-color: #ea868f; |
||||||
|
} |
||||||
|
|
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) { |
||||||
|
:root { |
||||||
|
scroll-behavior: smooth; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
font-family: var(--bs-body-font-family); |
||||||
|
font-size: var(--bs-body-font-size); |
||||||
|
font-weight: var(--bs-body-font-weight); |
||||||
|
line-height: var(--bs-body-line-height); |
||||||
|
color: var(--bs-body-color); |
||||||
|
text-align: var(--bs-body-text-align); |
||||||
|
background-color: var(--bs-body-bg); |
||||||
|
-webkit-text-size-adjust: 100%; |
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
hr { |
||||||
|
margin: 1rem 0; |
||||||
|
color: inherit; |
||||||
|
border: 0; |
||||||
|
border-top: var(--bs-border-width) solid; |
||||||
|
opacity: 0.25; |
||||||
|
} |
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-weight: 500; |
||||||
|
line-height: 1.2; |
||||||
|
color: var(--bs-heading-color); |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: calc(1.375rem + 1.5vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h1 { |
||||||
|
font-size: 2.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: calc(1.325rem + 0.9vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h2 { |
||||||
|
font-size: 2rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: calc(1.3rem + 0.6vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h3 { |
||||||
|
font-size: 1.75rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h4 { |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h4 { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h5 { |
||||||
|
font-size: 1.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
h6 { |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
abbr[title] { |
||||||
|
-webkit-text-decoration: underline dotted; |
||||||
|
text-decoration: underline dotted; |
||||||
|
cursor: help; |
||||||
|
-webkit-text-decoration-skip-ink: none; |
||||||
|
text-decoration-skip-ink: none; |
||||||
|
} |
||||||
|
|
||||||
|
address { |
||||||
|
margin-bottom: 1rem; |
||||||
|
font-style: normal; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul { |
||||||
|
padding-left: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul, |
||||||
|
dl { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol ol, |
||||||
|
ul ul, |
||||||
|
ol ul, |
||||||
|
ul ol { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
dt { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
dd { |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
margin-left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
blockquote { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
b, |
||||||
|
strong { |
||||||
|
font-weight: bolder; |
||||||
|
} |
||||||
|
|
||||||
|
small { |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
|
||||||
|
mark { |
||||||
|
padding: 0.1875em; |
||||||
|
color: var(--bs-highlight-color); |
||||||
|
background-color: var(--bs-highlight-bg); |
||||||
|
} |
||||||
|
|
||||||
|
sub, |
||||||
|
sup { |
||||||
|
position: relative; |
||||||
|
font-size: 0.75em; |
||||||
|
line-height: 0; |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
sub { |
||||||
|
bottom: -0.25em; |
||||||
|
} |
||||||
|
|
||||||
|
sup { |
||||||
|
top: -0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
a:hover { |
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); |
||||||
|
} |
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
pre, |
||||||
|
code, |
||||||
|
kbd, |
||||||
|
samp { |
||||||
|
font-family: var(--bs-font-monospace); |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
pre { |
||||||
|
display: block; |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
overflow: auto; |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
pre code { |
||||||
|
font-size: inherit; |
||||||
|
color: inherit; |
||||||
|
word-break: normal; |
||||||
|
} |
||||||
|
|
||||||
|
code { |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-code-color); |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
a > code { |
||||||
|
color: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
kbd { |
||||||
|
padding: 0.1875rem 0.375rem; |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-body-bg); |
||||||
|
background-color: var(--bs-body-color); |
||||||
|
border-radius: 0.25rem; |
||||||
|
} |
||||||
|
kbd kbd { |
||||||
|
padding: 0; |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
figure { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
img, |
||||||
|
svg { |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
table { |
||||||
|
caption-side: bottom; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
|
||||||
|
caption { |
||||||
|
padding-top: 0.5rem; |
||||||
|
padding-bottom: 0.5rem; |
||||||
|
color: var(--bs-secondary-color); |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
|
||||||
|
th { |
||||||
|
text-align: inherit; |
||||||
|
text-align: -webkit-match-parent; |
||||||
|
} |
||||||
|
|
||||||
|
thead, |
||||||
|
tbody, |
||||||
|
tfoot, |
||||||
|
tr, |
||||||
|
td, |
||||||
|
th { |
||||||
|
border-color: inherit; |
||||||
|
border-style: solid; |
||||||
|
border-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
label { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
border-radius: 0; |
||||||
|
} |
||||||
|
|
||||||
|
button:focus:not(:focus-visible) { |
||||||
|
outline: 0; |
||||||
|
} |
||||||
|
|
||||||
|
input, |
||||||
|
button, |
||||||
|
select, |
||||||
|
optgroup, |
||||||
|
textarea { |
||||||
|
margin: 0; |
||||||
|
font-family: inherit; |
||||||
|
font-size: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
select { |
||||||
|
text-transform: none; |
||||||
|
} |
||||||
|
|
||||||
|
[role=button] { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
word-wrap: normal; |
||||||
|
} |
||||||
|
select:disabled { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
[type=button], |
||||||
|
[type=reset], |
||||||
|
[type=submit] { |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
button:not(:disabled), |
||||||
|
[type=button]:not(:disabled), |
||||||
|
[type=reset]:not(:disabled), |
||||||
|
[type=submit]:not(:disabled) { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
::-moz-focus-inner { |
||||||
|
padding: 0; |
||||||
|
border-style: none; |
||||||
|
} |
||||||
|
|
||||||
|
textarea { |
||||||
|
resize: vertical; |
||||||
|
} |
||||||
|
|
||||||
|
fieldset { |
||||||
|
min-width: 0; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
legend { |
||||||
|
float: left; |
||||||
|
width: 100%; |
||||||
|
padding: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
legend { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
legend + * { |
||||||
|
clear: left; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper, |
||||||
|
::-webkit-datetime-edit-text, |
||||||
|
::-webkit-datetime-edit-minute, |
||||||
|
::-webkit-datetime-edit-hour-field, |
||||||
|
::-webkit-datetime-edit-day-field, |
||||||
|
::-webkit-datetime-edit-month-field, |
||||||
|
::-webkit-datetime-edit-year-field { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-inner-spin-button { |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
[type=search] { |
||||||
|
-webkit-appearance: textfield; |
||||||
|
outline-offset: -2px; |
||||||
|
} |
||||||
|
|
||||||
|
/* rtl:raw: |
||||||
|
[type="tel"], |
||||||
|
[type="url"], |
||||||
|
[type="email"], |
||||||
|
[type="number"] { |
||||||
|
direction: ltr; |
||||||
|
} |
||||||
|
*/ |
||||||
|
::-webkit-search-decoration { |
||||||
|
-webkit-appearance: none; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-file-upload-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
::file-selector-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
output { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
iframe { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
summary { |
||||||
|
display: list-item; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
progress { |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
[hidden] { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */ |
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,594 @@ |
|||||||
|
/*! |
||||||
|
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) |
||||||
|
* Copyright 2011-2024 The Bootstrap Authors |
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||||
|
*/ |
||||||
|
:root, |
||||||
|
[data-bs-theme=light] { |
||||||
|
--bs-blue: #0d6efd; |
||||||
|
--bs-indigo: #6610f2; |
||||||
|
--bs-purple: #6f42c1; |
||||||
|
--bs-pink: #d63384; |
||||||
|
--bs-red: #dc3545; |
||||||
|
--bs-orange: #fd7e14; |
||||||
|
--bs-yellow: #ffc107; |
||||||
|
--bs-green: #198754; |
||||||
|
--bs-teal: #20c997; |
||||||
|
--bs-cyan: #0dcaf0; |
||||||
|
--bs-black: #000; |
||||||
|
--bs-white: #fff; |
||||||
|
--bs-gray: #6c757d; |
||||||
|
--bs-gray-dark: #343a40; |
||||||
|
--bs-gray-100: #f8f9fa; |
||||||
|
--bs-gray-200: #e9ecef; |
||||||
|
--bs-gray-300: #dee2e6; |
||||||
|
--bs-gray-400: #ced4da; |
||||||
|
--bs-gray-500: #adb5bd; |
||||||
|
--bs-gray-600: #6c757d; |
||||||
|
--bs-gray-700: #495057; |
||||||
|
--bs-gray-800: #343a40; |
||||||
|
--bs-gray-900: #212529; |
||||||
|
--bs-primary: #0d6efd; |
||||||
|
--bs-secondary: #6c757d; |
||||||
|
--bs-success: #198754; |
||||||
|
--bs-info: #0dcaf0; |
||||||
|
--bs-warning: #ffc107; |
||||||
|
--bs-danger: #dc3545; |
||||||
|
--bs-light: #f8f9fa; |
||||||
|
--bs-dark: #212529; |
||||||
|
--bs-primary-rgb: 13, 110, 253; |
||||||
|
--bs-secondary-rgb: 108, 117, 125; |
||||||
|
--bs-success-rgb: 25, 135, 84; |
||||||
|
--bs-info-rgb: 13, 202, 240; |
||||||
|
--bs-warning-rgb: 255, 193, 7; |
||||||
|
--bs-danger-rgb: 220, 53, 69; |
||||||
|
--bs-light-rgb: 248, 249, 250; |
||||||
|
--bs-dark-rgb: 33, 37, 41; |
||||||
|
--bs-primary-text-emphasis: #052c65; |
||||||
|
--bs-secondary-text-emphasis: #2b2f32; |
||||||
|
--bs-success-text-emphasis: #0a3622; |
||||||
|
--bs-info-text-emphasis: #055160; |
||||||
|
--bs-warning-text-emphasis: #664d03; |
||||||
|
--bs-danger-text-emphasis: #58151c; |
||||||
|
--bs-light-text-emphasis: #495057; |
||||||
|
--bs-dark-text-emphasis: #495057; |
||||||
|
--bs-primary-bg-subtle: #cfe2ff; |
||||||
|
--bs-secondary-bg-subtle: #e2e3e5; |
||||||
|
--bs-success-bg-subtle: #d1e7dd; |
||||||
|
--bs-info-bg-subtle: #cff4fc; |
||||||
|
--bs-warning-bg-subtle: #fff3cd; |
||||||
|
--bs-danger-bg-subtle: #f8d7da; |
||||||
|
--bs-light-bg-subtle: #fcfcfd; |
||||||
|
--bs-dark-bg-subtle: #ced4da; |
||||||
|
--bs-primary-border-subtle: #9ec5fe; |
||||||
|
--bs-secondary-border-subtle: #c4c8cb; |
||||||
|
--bs-success-border-subtle: #a3cfbb; |
||||||
|
--bs-info-border-subtle: #9eeaf9; |
||||||
|
--bs-warning-border-subtle: #ffe69c; |
||||||
|
--bs-danger-border-subtle: #f1aeb5; |
||||||
|
--bs-light-border-subtle: #e9ecef; |
||||||
|
--bs-dark-border-subtle: #adb5bd; |
||||||
|
--bs-white-rgb: 255, 255, 255; |
||||||
|
--bs-black-rgb: 0, 0, 0; |
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); |
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif); |
||||||
|
--bs-body-font-size: 1rem; |
||||||
|
--bs-body-font-weight: 400; |
||||||
|
--bs-body-line-height: 1.5; |
||||||
|
--bs-body-color: #212529; |
||||||
|
--bs-body-color-rgb: 33, 37, 41; |
||||||
|
--bs-body-bg: #fff; |
||||||
|
--bs-body-bg-rgb: 255, 255, 255; |
||||||
|
--bs-emphasis-color: #000; |
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0; |
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75); |
||||||
|
--bs-secondary-color-rgb: 33, 37, 41; |
||||||
|
--bs-secondary-bg: #e9ecef; |
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239; |
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41; |
||||||
|
--bs-tertiary-bg: #f8f9fa; |
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #0d6efd; |
||||||
|
--bs-link-color-rgb: 13, 110, 253; |
||||||
|
--bs-link-decoration: underline; |
||||||
|
--bs-link-hover-color: #0a58ca; |
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202; |
||||||
|
--bs-code-color: #d63384; |
||||||
|
--bs-highlight-color: #212529; |
||||||
|
--bs-highlight-bg: #fff3cd; |
||||||
|
--bs-border-width: 1px; |
||||||
|
--bs-border-style: solid; |
||||||
|
--bs-border-color: #dee2e6; |
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); |
||||||
|
--bs-border-radius: 0.375rem; |
||||||
|
--bs-border-radius-sm: 0.25rem; |
||||||
|
--bs-border-radius-lg: 0.5rem; |
||||||
|
--bs-border-radius-xl: 1rem; |
||||||
|
--bs-border-radius-xxl: 2rem; |
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl); |
||||||
|
--bs-border-radius-pill: 50rem; |
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); |
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); |
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); |
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); |
||||||
|
--bs-focus-ring-width: 0.25rem; |
||||||
|
--bs-focus-ring-opacity: 0.25; |
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); |
||||||
|
--bs-form-valid-color: #198754; |
||||||
|
--bs-form-valid-border-color: #198754; |
||||||
|
--bs-form-invalid-color: #dc3545; |
||||||
|
--bs-form-invalid-border-color: #dc3545; |
||||||
|
} |
||||||
|
|
||||||
|
[data-bs-theme=dark] { |
||||||
|
color-scheme: dark; |
||||||
|
--bs-body-color: #dee2e6; |
||||||
|
--bs-body-color-rgb: 222, 226, 230; |
||||||
|
--bs-body-bg: #212529; |
||||||
|
--bs-body-bg-rgb: 33, 37, 41; |
||||||
|
--bs-emphasis-color: #fff; |
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255; |
||||||
|
--bs-secondary-color: rgba(222, 226, 230, 0.75); |
||||||
|
--bs-secondary-color-rgb: 222, 226, 230; |
||||||
|
--bs-secondary-bg: #343a40; |
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64; |
||||||
|
--bs-tertiary-color: rgba(222, 226, 230, 0.5); |
||||||
|
--bs-tertiary-color-rgb: 222, 226, 230; |
||||||
|
--bs-tertiary-bg: #2b3035; |
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53; |
||||||
|
--bs-primary-text-emphasis: #6ea8fe; |
||||||
|
--bs-secondary-text-emphasis: #a7acb1; |
||||||
|
--bs-success-text-emphasis: #75b798; |
||||||
|
--bs-info-text-emphasis: #6edff6; |
||||||
|
--bs-warning-text-emphasis: #ffda6a; |
||||||
|
--bs-danger-text-emphasis: #ea868f; |
||||||
|
--bs-light-text-emphasis: #f8f9fa; |
||||||
|
--bs-dark-text-emphasis: #dee2e6; |
||||||
|
--bs-primary-bg-subtle: #031633; |
||||||
|
--bs-secondary-bg-subtle: #161719; |
||||||
|
--bs-success-bg-subtle: #051b11; |
||||||
|
--bs-info-bg-subtle: #032830; |
||||||
|
--bs-warning-bg-subtle: #332701; |
||||||
|
--bs-danger-bg-subtle: #2c0b0e; |
||||||
|
--bs-light-bg-subtle: #343a40; |
||||||
|
--bs-dark-bg-subtle: #1a1d20; |
||||||
|
--bs-primary-border-subtle: #084298; |
||||||
|
--bs-secondary-border-subtle: #41464b; |
||||||
|
--bs-success-border-subtle: #0f5132; |
||||||
|
--bs-info-border-subtle: #087990; |
||||||
|
--bs-warning-border-subtle: #997404; |
||||||
|
--bs-danger-border-subtle: #842029; |
||||||
|
--bs-light-border-subtle: #495057; |
||||||
|
--bs-dark-border-subtle: #343a40; |
||||||
|
--bs-heading-color: inherit; |
||||||
|
--bs-link-color: #6ea8fe; |
||||||
|
--bs-link-hover-color: #8bb9fe; |
||||||
|
--bs-link-color-rgb: 110, 168, 254; |
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254; |
||||||
|
--bs-code-color: #e685b5; |
||||||
|
--bs-highlight-color: #dee2e6; |
||||||
|
--bs-highlight-bg: #664d03; |
||||||
|
--bs-border-color: #495057; |
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); |
||||||
|
--bs-form-valid-color: #75b798; |
||||||
|
--bs-form-valid-border-color: #75b798; |
||||||
|
--bs-form-invalid-color: #ea868f; |
||||||
|
--bs-form-invalid-border-color: #ea868f; |
||||||
|
} |
||||||
|
|
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) { |
||||||
|
:root { |
||||||
|
scroll-behavior: smooth; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
font-family: var(--bs-body-font-family); |
||||||
|
font-size: var(--bs-body-font-size); |
||||||
|
font-weight: var(--bs-body-font-weight); |
||||||
|
line-height: var(--bs-body-line-height); |
||||||
|
color: var(--bs-body-color); |
||||||
|
text-align: var(--bs-body-text-align); |
||||||
|
background-color: var(--bs-body-bg); |
||||||
|
-webkit-text-size-adjust: 100%; |
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
hr { |
||||||
|
margin: 1rem 0; |
||||||
|
color: inherit; |
||||||
|
border: 0; |
||||||
|
border-top: var(--bs-border-width) solid; |
||||||
|
opacity: 0.25; |
||||||
|
} |
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-weight: 500; |
||||||
|
line-height: 1.2; |
||||||
|
color: var(--bs-heading-color); |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: calc(1.375rem + 1.5vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h1 { |
||||||
|
font-size: 2.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: calc(1.325rem + 0.9vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h2 { |
||||||
|
font-size: 2rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: calc(1.3rem + 0.6vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h3 { |
||||||
|
font-size: 1.75rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h4 { |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h4 { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h5 { |
||||||
|
font-size: 1.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
h6 { |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
abbr[title] { |
||||||
|
-webkit-text-decoration: underline dotted; |
||||||
|
text-decoration: underline dotted; |
||||||
|
cursor: help; |
||||||
|
-webkit-text-decoration-skip-ink: none; |
||||||
|
text-decoration-skip-ink: none; |
||||||
|
} |
||||||
|
|
||||||
|
address { |
||||||
|
margin-bottom: 1rem; |
||||||
|
font-style: normal; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul { |
||||||
|
padding-right: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul, |
||||||
|
dl { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol ol, |
||||||
|
ul ul, |
||||||
|
ol ul, |
||||||
|
ul ol { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
dt { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
dd { |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
blockquote { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
b, |
||||||
|
strong { |
||||||
|
font-weight: bolder; |
||||||
|
} |
||||||
|
|
||||||
|
small { |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
|
||||||
|
mark { |
||||||
|
padding: 0.1875em; |
||||||
|
color: var(--bs-highlight-color); |
||||||
|
background-color: var(--bs-highlight-bg); |
||||||
|
} |
||||||
|
|
||||||
|
sub, |
||||||
|
sup { |
||||||
|
position: relative; |
||||||
|
font-size: 0.75em; |
||||||
|
line-height: 0; |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
sub { |
||||||
|
bottom: -0.25em; |
||||||
|
} |
||||||
|
|
||||||
|
sup { |
||||||
|
top: -0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
a:hover { |
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); |
||||||
|
} |
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
pre, |
||||||
|
code, |
||||||
|
kbd, |
||||||
|
samp { |
||||||
|
font-family: var(--bs-font-monospace); |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
pre { |
||||||
|
display: block; |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
overflow: auto; |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
pre code { |
||||||
|
font-size: inherit; |
||||||
|
color: inherit; |
||||||
|
word-break: normal; |
||||||
|
} |
||||||
|
|
||||||
|
code { |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-code-color); |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
a > code { |
||||||
|
color: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
kbd { |
||||||
|
padding: 0.1875rem 0.375rem; |
||||||
|
font-size: 0.875em; |
||||||
|
color: var(--bs-body-bg); |
||||||
|
background-color: var(--bs-body-color); |
||||||
|
border-radius: 0.25rem; |
||||||
|
} |
||||||
|
kbd kbd { |
||||||
|
padding: 0; |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
figure { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
img, |
||||||
|
svg { |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
table { |
||||||
|
caption-side: bottom; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
|
||||||
|
caption { |
||||||
|
padding-top: 0.5rem; |
||||||
|
padding-bottom: 0.5rem; |
||||||
|
color: var(--bs-secondary-color); |
||||||
|
text-align: right; |
||||||
|
} |
||||||
|
|
||||||
|
th { |
||||||
|
text-align: inherit; |
||||||
|
text-align: -webkit-match-parent; |
||||||
|
} |
||||||
|
|
||||||
|
thead, |
||||||
|
tbody, |
||||||
|
tfoot, |
||||||
|
tr, |
||||||
|
td, |
||||||
|
th { |
||||||
|
border-color: inherit; |
||||||
|
border-style: solid; |
||||||
|
border-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
label { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
border-radius: 0; |
||||||
|
} |
||||||
|
|
||||||
|
button:focus:not(:focus-visible) { |
||||||
|
outline: 0; |
||||||
|
} |
||||||
|
|
||||||
|
input, |
||||||
|
button, |
||||||
|
select, |
||||||
|
optgroup, |
||||||
|
textarea { |
||||||
|
margin: 0; |
||||||
|
font-family: inherit; |
||||||
|
font-size: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
select { |
||||||
|
text-transform: none; |
||||||
|
} |
||||||
|
|
||||||
|
[role=button] { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
word-wrap: normal; |
||||||
|
} |
||||||
|
select:disabled { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
[type=button], |
||||||
|
[type=reset], |
||||||
|
[type=submit] { |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
button:not(:disabled), |
||||||
|
[type=button]:not(:disabled), |
||||||
|
[type=reset]:not(:disabled), |
||||||
|
[type=submit]:not(:disabled) { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
::-moz-focus-inner { |
||||||
|
padding: 0; |
||||||
|
border-style: none; |
||||||
|
} |
||||||
|
|
||||||
|
textarea { |
||||||
|
resize: vertical; |
||||||
|
} |
||||||
|
|
||||||
|
fieldset { |
||||||
|
min-width: 0; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
legend { |
||||||
|
float: right; |
||||||
|
width: 100%; |
||||||
|
padding: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
legend { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
legend + * { |
||||||
|
clear: right; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper, |
||||||
|
::-webkit-datetime-edit-text, |
||||||
|
::-webkit-datetime-edit-minute, |
||||||
|
::-webkit-datetime-edit-hour-field, |
||||||
|
::-webkit-datetime-edit-day-field, |
||||||
|
::-webkit-datetime-edit-month-field, |
||||||
|
::-webkit-datetime-edit-year-field { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-inner-spin-button { |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
[type=search] { |
||||||
|
-webkit-appearance: textfield; |
||||||
|
outline-offset: -2px; |
||||||
|
} |
||||||
|
|
||||||
|
[type="tel"], |
||||||
|
[type="url"], |
||||||
|
[type="email"], |
||||||
|
[type="number"] { |
||||||
|
direction: ltr; |
||||||
|
} |
||||||
|
::-webkit-search-decoration { |
||||||
|
-webkit-appearance: none; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-file-upload-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
::file-selector-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
output { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
iframe { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
summary { |
||||||
|
display: list-item; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
progress { |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
[hidden] { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ |
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue