fix(ui): invariant decimals + LocaleState test isolation + drawer offset + bundled Plotly
Cross-cutting polish that surfaced while Phase 8 was being implemented:
* Invariant-culture formatting on every decimal ToString("0.00" / "0.##")
(OddsCell, OddsTimeline, SeverityBadge, AnomalyEvidence,
Pages/Events/Detail) — previously the comma/dot decimal separator
switched with the locale and broke tabular alignment + tests.
* LocaleState constructor no longer mutates process-wide ambient culture;
apply only happens through Set(...). Stops parallel bUnit test runs
leaking ru-RU into each other's threads.
* MainLayout: drawer-open state now offsets main content / footer / appbar
by the drawer width (248px) so the sidebar no longer overlaps content.
Mobile breakpoint (≤720px) keeps the original full-width layout.
* wwwroot/index.html (Marathon.UI RCL): switched from Plotly CDN to the
bundled "_content/Plotly.Blazor/plotly-2.35.3.min.js" — works offline
and matches the Plotly.Blazor 5.4.1 version pin.
* Marathon.Hosts.WpfBlazor/wwwroot/index.html: host-level page that
BlazorWebView's HostPage attribute resolves to. Same Plotly bundle,
no autostart="false" (BlazorWebView auto-starts).
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
|
<title>Marathon — Odds Lab</title>
|
||||||
|
<base href="/" />
|
||||||
|
|
||||||
|
<!-- Prevent flash of unthemed content -->
|
||||||
|
<style>
|
||||||
|
html, body { background: #f5f4ef; margin: 0; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html, body { background: #0c0a09; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Fonts: IBM Plex Sans / Serif / Mono + JetBrains Mono. Full Cyrillic coverage. -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=IBM+Plex+Serif:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
||||||
|
rel="stylesheet" />
|
||||||
|
|
||||||
|
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||||
|
<link href="_content/Marathon.UI/app.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div style="padding: 64px; font-family: 'IBM Plex Serif', Georgia, serif; color: #475569;">
|
||||||
|
<span style="font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: #d97706;">Booting</span>
|
||||||
|
<div style="font-size: 32px; font-weight: 300; margin-top: 8px;">Marathon Odds Lab</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui" style="display:none; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; padding: 12px 24px; background: #dc2626; color: #fafaf7; font-family: 'IBM Plex Sans', sans-serif;">
|
||||||
|
<span>An unhandled error has occurred.</span>
|
||||||
|
<a href="" class="reload" style="color: #fff; text-decoration: underline; margin-left: 12px;">Reload</a>
|
||||||
|
<a class="dismiss" style="float: right; cursor: pointer; padding: 0 8px;">×</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plotly.js for OddsTimeline charts (Phase 6). Bundled by Plotly.Blazor 5.4.1. -->
|
||||||
|
<script src="_content/Plotly.Blazor/plotly-2.35.3.min.js" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Blazor must auto-start in BlazorWebView; no autostart="false". -->
|
||||||
|
<script src="_framework/blazor.webview.js"></script>
|
||||||
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
|
|
||||||
builder.OpenElement(12, "span");
|
builder.OpenElement(12, "span");
|
||||||
builder.AddAttribute(13, "class", "m-evidence__rate");
|
builder.AddAttribute(13, "class", "m-evidence__rate");
|
||||||
builder.AddContent(14, rate is { } r ? r.ToString("0.00") : "—");
|
builder.AddContent(14, rate is { } r ? r.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) : "—");
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
|
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
|
|||||||
@@ -87,7 +87,9 @@
|
|||||||
[Parameter] public string AriaPrefix { get; set; } = "Odds";
|
[Parameter] public string AriaPrefix { get; set; } = "Odds";
|
||||||
[Parameter] public string EmptyPlaceholder { get; set; } = "—";
|
[Parameter] public string EmptyPlaceholder { get; set; } = "—";
|
||||||
|
|
||||||
private string Formatted => Rate is { } r ? r.ToString("0.00") : EmptyPlaceholder;
|
private string Formatted => Rate is { } r
|
||||||
|
? r.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
: EmptyPlaceholder;
|
||||||
|
|
||||||
private Trend Direction
|
private Trend Direction
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -238,5 +238,6 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string FormatRate(decimal? r) => r is { } v ? v.ToString("0.00") : "—";
|
private static string FormatRate(decimal? r) =>
|
||||||
|
r is { } v ? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) : "—";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<span class="m-severity__label">@Label</span>
|
<span class="m-severity__label">@Label</span>
|
||||||
@if (ShowScore && Score is { } s)
|
@if (ShowScore && Score is { } s)
|
||||||
{
|
{
|
||||||
<span class="m-severity__score m-mono" aria-hidden="true">@s.ToString("0.00")</span>
|
<span class="m-severity__score m-mono" aria-hidden="true">@s.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,25 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* MudDrawer is positioned fixed/absolute by Mud's CSS — push main content
|
||||||
|
right by the drawer's width (248px) so the sidebar doesn't overlap.
|
||||||
|
Same shift on the appbar tools row so the brand isn't covered. */
|
||||||
|
.m-app-frame.is-drawer-open .m-main,
|
||||||
|
.m-app-frame.is-drawer-open .m-footer {
|
||||||
|
padding-left: 248px;
|
||||||
|
transition: padding-left 200ms ease;
|
||||||
|
}
|
||||||
|
.m-app-frame.is-drawer-open .m-appbar {
|
||||||
|
padding-left: calc(248px + clamp(var(--m-space-3), 2vw, var(--m-space-5)));
|
||||||
|
transition: padding-left 200ms ease;
|
||||||
|
}
|
||||||
|
@@media (max-width: 720px) {
|
||||||
|
/* On narrow viewports the drawer becomes a temporary overlay anyway. */
|
||||||
|
.m-app-frame.is-drawer-open .m-main,
|
||||||
|
.m-app-frame.is-drawer-open .m-footer,
|
||||||
|
.m-app-frame.is-drawer-open .m-appbar { padding-left: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
.m-footer {
|
.m-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
<td>@BetTypeLabel(row.Type)</td>
|
<td>@BetTypeLabel(row.Type)</td>
|
||||||
<td>@SideLabel(row.Side)</td>
|
<td>@SideLabel(row.Side)</td>
|
||||||
<td class="m-mono" style="text-align: right;">@FormatThreshold(row.Threshold)</td>
|
<td class="m-mono" style="text-align: right;">@FormatThreshold(row.Threshold)</td>
|
||||||
<td class="m-mono" style="text-align: right; font-weight: 500;">@row.Rate.ToString("0.00")</td>
|
<td class="m-mono" style="text-align: right; font-weight: 500;">@row.Rate.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -334,6 +334,8 @@
|
|||||||
_ => s.ToString(),
|
_ => s.ToString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string FormatRate(decimal? r) => r is { } v ? v.ToString("0.00") : "—";
|
private static string FormatRate(decimal? r) =>
|
||||||
private static string FormatThreshold(decimal? v) => v is { } x ? x.ToString("0.##") : "—";
|
r is { } v ? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) : "—";
|
||||||
|
private static string FormatThreshold(decimal? v) =>
|
||||||
|
v is { } x ? x.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture) : "—";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,23 +15,35 @@ public sealed class LocaleState
|
|||||||
|
|
||||||
public static readonly IReadOnlyList<string> Supported = new[] { Russian, English };
|
public static readonly IReadOnlyList<string> Supported = new[] { Russian, English };
|
||||||
|
|
||||||
private CultureInfo _culture = CultureInfo.GetCultureInfo(Russian);
|
private CultureInfo _culture;
|
||||||
|
|
||||||
|
public LocaleState()
|
||||||
|
{
|
||||||
|
// Initialise the field only — do NOT mutate process-wide ambient culture
|
||||||
|
// here. Production startup (App.xaml.cs) always calls Set(...) right after
|
||||||
|
// resolving this service, which applies ambient culture explicitly. Doing
|
||||||
|
// it here as well leaks ru-RU across parallel test runs whenever any
|
||||||
|
// bUnit test class instantiates MarathonTestContext.
|
||||||
|
_culture = CultureInfo.GetCultureInfo(Russian);
|
||||||
|
}
|
||||||
|
|
||||||
public CultureInfo Culture
|
public CultureInfo Culture
|
||||||
{
|
{
|
||||||
get => _culture;
|
get => _culture;
|
||||||
private set
|
private set
|
||||||
{
|
{
|
||||||
|
// Always re-apply ambient cultures even when the value matches —
|
||||||
|
// App.xaml.cs's startup call uses the same default the constructor
|
||||||
|
// initialized to, so an equality short-circuit here would leave the
|
||||||
|
// actual thread CurrentCulture stuck on the system locale.
|
||||||
|
ApplyAmbientCulture(value);
|
||||||
|
|
||||||
if (string.Equals(_culture.Name, value.Name, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(_culture.Name, value.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_culture = value;
|
_culture = value;
|
||||||
CultureInfo.DefaultThreadCurrentCulture = value;
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = value;
|
|
||||||
CultureInfo.CurrentCulture = value;
|
|
||||||
CultureInfo.CurrentUICulture = value;
|
|
||||||
OnChange?.Invoke();
|
OnChange?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,4 +59,12 @@ public sealed class LocaleState
|
|||||||
|
|
||||||
Culture = CultureInfo.GetCultureInfo(cultureName);
|
Culture = CultureInfo.GetCultureInfo(cultureName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplyAmbientCulture(CultureInfo c)
|
||||||
|
{
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = c;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = c;
|
||||||
|
CultureInfo.CurrentCulture = c;
|
||||||
|
CultureInfo.CurrentUICulture = c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
<a class="dismiss" style="float: right; cursor: pointer; padding: 0 8px;">×</a>
|
<a class="dismiss" style="float: right; cursor: pointer; padding: 0 8px;">×</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plotly.js for OddsTimeline charts (Phase 6). Pinned major version 2.x. -->
|
<!-- Plotly.js for OddsTimeline charts (Phase 6). Bundled by Plotly.Blazor 5.4.1. -->
|
||||||
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script>
|
<script src="_content/Plotly.Blazor/plotly-2.35.3.min.js" charset="utf-8"></script>
|
||||||
|
|
||||||
<script src="_framework/blazor.webview.js" autostart="false"></script>
|
<script src="_framework/blazor.webview.js" autostart="false"></script>
|
||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user