SF-74: Client UI — MainLayout HeadContent+footer theme controls, ThemeControls component, app.css sf-footer styles
This commit is contained in:
@@ -15,44 +15,13 @@
|
|||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<footer class="receiver-footer">
|
<footer class="receiver-footer">
|
||||||
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
<div class="receiver-footer__left">
|
||||||
<span class="receiver-footer__sep">|</span>
|
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||||
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
<span class="receiver-footer__sep">|</span>
|
||||||
|
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||||
<div class="sf-footer-controls">
|
|
||||||
<select class="sf-footer-select"
|
|
||||||
title="Theme wählen"
|
|
||||||
@onchange="OnThemeChanged">
|
|
||||||
@foreach (var theme in ThemeService.AvailableThemes)
|
|
||||||
{
|
|
||||||
<option value="@theme.Value" selected="@(theme.Value == ThemeService.ThemeName)">
|
|
||||||
@theme.Label
|
|
||||||
</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
<button class="sf-footer-toggle"
|
|
||||||
@onclick="OnToggleDarkMode"
|
|
||||||
title="@(ThemeService.IsDarkMode ? "Light Mode" : "Dark Mode")">
|
|
||||||
@if (ThemeService.IsDarkMode)
|
|
||||||
{
|
|
||||||
@* Sun *@
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
||||||
<circle cx="12" cy="12" r="5" fill="currentColor" stroke="none"/>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@* Moon *@
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ThemeControls />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,22 +38,6 @@
|
|||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
await ThemeService.InitializeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnToggleDarkMode()
|
|
||||||
=> await ThemeService.ToggleDarkModeAsync();
|
|
||||||
|
|
||||||
private async Task OnThemeChanged(ChangeEventArgs e)
|
|
||||||
{
|
|
||||||
var value = e.Value?.ToString();
|
|
||||||
if (!string.IsNullOrEmpty(value))
|
|
||||||
await ThemeService.SetThemeAsync(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> ThemeService.OnChange -= _themeChangeHandler;
|
=> ThemeService.OnChange -= _themeChangeHandler;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject ThemeService ThemeService
|
||||||
|
|
||||||
|
<div class="sf-theme-slider">
|
||||||
|
|
||||||
|
@* Moon — left end (blazing-dark) *@
|
||||||
|
<svg class="sf-theme-icon @(ThemeService.ThemeName == "blazing-dark" ? "sf-theme-icon--active" : "")"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 24 24"
|
||||||
|
title="Blazing Dark">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
@* Track wrapper: range input + left/mid/right ticks *@
|
||||||
|
<div class="sf-theme-track-wrapper">
|
||||||
|
@* Left tick *@
|
||||||
|
<div class="sf-theme-tick sf-theme-tick--left"></div>
|
||||||
|
@* Mid tick (marks the middle position) *@
|
||||||
|
<div class="sf-theme-tick sf-theme-tick--mid"></div>
|
||||||
|
@* Right tick *@
|
||||||
|
<div class="sf-theme-tick sf-theme-tick--right"></div>
|
||||||
|
|
||||||
|
<input type="range"
|
||||||
|
class="sf-theme-range"
|
||||||
|
min="0" max="2" step="1"
|
||||||
|
value="@SliderValue"
|
||||||
|
@oninput="OnSliderChanged"
|
||||||
|
title="@ThemeService.AvailableThemes[SliderValue].Label" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Sun — right end (purple) *@
|
||||||
|
<svg class="sf-theme-icon @(ThemeService.ThemeName == "purple" ? "sf-theme-icon--active" : (ThemeService.ThemeName == "blazing-berry" ? "sf-theme-icon--mid" : ""))"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor" stroke-width="1.5"
|
||||||
|
title="Blazing Berry / Purple">
|
||||||
|
<circle cx="12" cy="12" r="4" fill="currentColor" stroke="none"/>
|
||||||
|
<line x1="12" y1="2" x2="12" y2="5"/>
|
||||||
|
<line x1="12" y1="19" x2="12" y2="22"/>
|
||||||
|
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/>
|
||||||
|
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/>
|
||||||
|
<line x1="2" y1="12" x2="5" y2="12"/>
|
||||||
|
<line x1="19" y1="12" x2="22" y2="12"/>
|
||||||
|
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/>
|
||||||
|
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private Action? _themeChangeHandler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the active theme to a 0-based slider step.
|
||||||
|
/// 0 = blazing-dark (🌙 left)
|
||||||
|
/// 1 = blazing-berry (middle)
|
||||||
|
/// 2 = purple (☀️ right)
|
||||||
|
/// </summary>
|
||||||
|
private int SliderValue => ThemeService.ThemeName switch
|
||||||
|
{
|
||||||
|
"blazing-dark" => 0,
|
||||||
|
"purple" => 2,
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_themeChangeHandler = () => InvokeAsync(StateHasChanged);
|
||||||
|
ThemeService.OnChange += _themeChangeHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
await ThemeService.InitializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSliderChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(e.Value?.ToString(), out var step)) return;
|
||||||
|
|
||||||
|
var themeName = step switch
|
||||||
|
{
|
||||||
|
0 => "blazing-dark",
|
||||||
|
2 => "purple",
|
||||||
|
_ => "blazing-berry"
|
||||||
|
};
|
||||||
|
|
||||||
|
await ThemeService.SetThemeAsync(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> ThemeService.OnChange -= _themeChangeHandler;
|
||||||
|
}
|
||||||
@@ -342,7 +342,7 @@ article {
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between; /* ← left group | right group */
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||||
@@ -363,65 +363,136 @@ article {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receiver-footer__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.receiver-footer__sep {
|
.receiver-footer__sep {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Footer: Theme & Dark Mode Controls ─────────────────────────────────── */
|
/* ── Footer: Theme Slider ────────────────────────────────────────────────── */
|
||||||
/* Positioned at the right end of the fixed footer bar. */
|
/* 🌙 blazing-dark ──◇──·──◇── purple ☀️ */
|
||||||
/* Styled to match the footer's gradient background — small, unobtrusive. */
|
/* Left group: copyright | datenschutz → Right group: slider */
|
||||||
|
|
||||||
.sf-footer-controls {
|
.sf-theme-slider {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 7px;
|
||||||
margin-left: auto; /* push to the right */
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-footer-select {
|
|
||||||
height: 22px;
|
|
||||||
padding: 0 5px;
|
|
||||||
font-size: 0.68rem;
|
|
||||||
line-height: 1;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
/* Native select options use browser defaults — acceptable in a footer context */
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-footer-select:hover,
|
|
||||||
.sf-footer-select:focus {
|
|
||||||
background: rgba(255, 255, 255, 0.20);
|
|
||||||
border-color: rgba(255, 255, 255, 0.38);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force option text to be readable in browsers that support it */
|
|
||||||
.sf-footer-select option {
|
|
||||||
color: #1e2e3e;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-footer-toggle {
|
|
||||||
width: 24px;
|
|
||||||
height: 22px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, border-color 0.15s;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-footer-toggle:hover {
|
/* Moon / Sun SVG icons */
|
||||||
background: rgba(255, 255, 255, 0.24);
|
.sf-theme-icon {
|
||||||
border-color: rgba(255, 255, 255, 0.40);
|
display: block;
|
||||||
color: #ffffff;
|
opacity: 0.55; /* ↑ was 0.42 — more visible as base */
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.18s;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-theme-icon--active {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-theme-icon--mid {
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Wrapper holds the range input + tick marks ───────────────────────────── */
|
||||||
|
.sf-theme-track-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 80px;
|
||||||
|
height: 16px; /* enough height so ticks + thumb don't clip */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actual range input — fills the wrapper */
|
||||||
|
.sf-theme-range {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.28);
|
||||||
|
border-radius: 1px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
/* no extra padding so ticks align correctly */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-theme-range:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thumb — white circle */
|
||||||
|
.sf-theme-range::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.90);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
|
||||||
|
transition: transform 0.12s, box-shadow 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-theme-range::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.25);
|
||||||
|
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
.sf-theme-range::-moz-range-thumb {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.90);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-theme-range::-moz-range-track {
|
||||||
|
height: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.28);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── End ticks (left & right): thin vertical bars ────────────────────────── */
|
||||||
|
.sf-theme-tick {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
width: 2px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.45);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-theme-tick--left { left: 0; }
|
||||||
|
.sf-theme-tick--right { right: 0; }
|
||||||
|
|
||||||
|
/* Mid tick — thinner, subtler */
|
||||||
|
.sf-theme-tick--mid {
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 1px;
|
||||||
|
height: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.50); /* ↑ was 0.22 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sun icon: always visible regardless of active state */
|
||||||
|
.sf-theme-icon--sun {
|
||||||
|
opacity: 0.72; /* minimum visibility */
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user