...
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
@implements IDisposable
|
||||
|
||||
<div class="cooldown-wrap" style="--cooldown-size: @(Size)px">
|
||||
<button class="cooldown-btn"
|
||||
disabled="@_isCooldown"
|
||||
@onclick="HandleClick"
|
||||
@onclick:preventDefault="_isCooldown">
|
||||
<span class="cooldown-btn-text">@ChildContent</span>
|
||||
</button>
|
||||
@if (_isCooldown)
|
||||
{
|
||||
<div class="cooldown-overlay"
|
||||
style="mask-image: conic-gradient(transparent 0deg, transparent @(_elapsedAngle)deg, #000 @(_elapsedAngle)deg, #000 360deg);
|
||||
-webkit-mask-image: conic-gradient(transparent 0deg, transparent @(_elapsedAngle)deg, #000 @(_elapsedAngle)deg, #000 360deg);">
|
||||
<span class="cooldown-label">@_remainingSeconds</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cooldown-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: var(--cooldown-size, 120px);
|
||||
height: var(--cooldown-size, 120px);
|
||||
}
|
||||
.cooldown-btn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid var(--primary);
|
||||
border-radius: 12px;
|
||||
background: var(--paper);
|
||||
color: var(--text-primary, #eee);
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
line-height: 1.3;
|
||||
font-family: inherit;
|
||||
}
|
||||
.cooldown-btn:hover:not(:disabled) {
|
||||
background: var(--paper-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
.cooldown-btn:active:not(:disabled) {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
.cooldown-btn:disabled {
|
||||
opacity: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.cooldown-btn-text {
|
||||
pointer-events: none;
|
||||
}
|
||||
.cooldown-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 12px;
|
||||
background: rgba(22, 22, 24, 0.82);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.cooldown-label {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 900;
|
||||
color: #999;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
[Parameter] public EventCallback OnClick { get; set; }
|
||||
[Parameter] public int CooldownSeconds { get; set; } = 12;
|
||||
[Parameter] public int Size { get; set; } = 120;
|
||||
|
||||
private bool _isCooldown;
|
||||
private int _elapsedAngle;
|
||||
private int _remainingSeconds;
|
||||
private DateTime _startTime;
|
||||
private System.Timers.Timer? _timer;
|
||||
|
||||
private async Task HandleClick()
|
||||
{
|
||||
if (_isCooldown) return;
|
||||
await OnClick.InvokeAsync(null);
|
||||
StartCooldown();
|
||||
}
|
||||
|
||||
private void StartCooldown()
|
||||
{
|
||||
_isCooldown = true;
|
||||
_startTime = DateTime.UtcNow;
|
||||
_elapsedAngle = 0;
|
||||
_remainingSeconds = CooldownSeconds;
|
||||
|
||||
_timer = new System.Timers.Timer(33);
|
||||
_timer.Elapsed += OnTick;
|
||||
_timer.AutoReset = true;
|
||||
_timer.Enabled = true;
|
||||
}
|
||||
|
||||
private void OnTick(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - _startTime).TotalSeconds;
|
||||
if (elapsed >= CooldownSeconds)
|
||||
{
|
||||
_isCooldown = false;
|
||||
_timer?.Stop();
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
_elapsedAngle = (int)(elapsed / CooldownSeconds * 360);
|
||||
_remainingSeconds = CooldownSeconds - (int)elapsed;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_timer != null)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,15 @@
|
||||
<div>
|
||||
Refer to various aspects of "IMMORTAL: Gates of Pyre" from this external reference!
|
||||
</div>
|
||||
|
||||
<div class="cooldown-demo">
|
||||
<div class="cooldown-demo-label">Cooldown Demo</div>
|
||||
<CooldownButtonComponent CooldownSeconds="12"
|
||||
Size="120"
|
||||
OnClick="OnCooldownClick">
|
||||
Click Me
|
||||
</CooldownButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
</PaperComponent>
|
||||
|
||||
@@ -42,6 +51,18 @@
|
||||
.mainContainer {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.cooldown-demo {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.cooldown-demo-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #aaa);
|
||||
}
|
||||
|
||||
.mainTitle {
|
||||
font-size: 2.2rem;
|
||||
@@ -62,4 +83,11 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private Task OnCooldownClick()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Vendored
+5
-5
@@ -42,12 +42,12 @@
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Build Calculator CmdLine.md",
|
||||
"file": "Feature Proposals.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Build Calculator CmdLine"
|
||||
"title": "Feature Proposals"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -225,10 +225,11 @@
|
||||
},
|
||||
"active": "68e1ba2b54081b9a",
|
||||
"lastOpenFiles": [
|
||||
"Device MAUI Pages Setup.md",
|
||||
"cooldown-button.md",
|
||||
"Build Calculator CmdLine.md",
|
||||
"Feature Proposals.md",
|
||||
"_Tasks Kanban.base",
|
||||
"Device MAUI Pages Setup.md",
|
||||
"Feature Proposals.md",
|
||||
"Tasks/Worker Income UI and Tests.md",
|
||||
"Tasks/Update the Reference Tables with Telerik.md",
|
||||
"Tasks/WebAssembly back to Azure.md",
|
||||
@@ -251,7 +252,6 @@
|
||||
"Tasks/Highest Alloy and Ether Tests.md",
|
||||
"Tasks/Get AI to Add easy Test Tasks.md",
|
||||
"Tasks/Helper Tutorial Info Improvements.md",
|
||||
"Tasks/Fix Entity Recursion Error - Parent.md",
|
||||
"Tasks",
|
||||
"Images/Pasted image 20260601093510.png",
|
||||
"Images/Pasted image 20260601083333.png",
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Cooldown Button Component
|
||||
|
||||
A square Blazor button with a 12-second cooldown animation: when clicked, the button greys out and a circular transparency wedge dials open clockwise, progressively revealing the normal button state underneath.
|
||||
|
||||
---
|
||||
|
||||
## Visual Design
|
||||
|
||||
The button has two visual states:
|
||||
|
||||
**Idle state** – A solid square button with the project's standard `--paper` background and `--primary` border. Hover inverts or brightens the paper. Click triggers a brief scale-down.
|
||||
|
||||
**Cooldown state** – The native button fades to `opacity: 0` while an absolutely-positioned overlay covers it. The overlay uses a `conic-gradient` CSS mask to create a "dialling open" effect. A number in the centre shows the remaining seconds.
|
||||
|
||||
---
|
||||
|
||||
## Core Technique: Conic-Gradient Mask
|
||||
|
||||
The cooldown reveal is accomplished with a **conic-gradient mask** applied to the overlay `<div>`:
|
||||
|
||||
```
|
||||
mask-image: conic-gradient(
|
||||
transparent 0deg,
|
||||
transparent {angle}deg,
|
||||
#000 {angle}deg,
|
||||
#000 360deg
|
||||
);
|
||||
```
|
||||
|
||||
- **`transparent`** – lets the button underneath show through (revealed area)
|
||||
- **`#000` (black)** – fully masks the overlay, making it visible (greyed-out area)
|
||||
|
||||
At `angle = 0deg`, transparent covers nothing and the mask is entirely black → the overlay is fully opaque (button completely greyed out).
|
||||
|
||||
At `angle = 360deg`, transparent covers the full circle and black covers nothing → the overlay is fully transparent (button completely visible).
|
||||
|
||||
The angle animates linearly from 0 to 360 over the cooldown duration. Because `conic-gradient` starts at the 12 o'clock position and sweeps clockwise, the reveal begins at the top of the button and rotates around, like a clock hand or a dial opening.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
### Component Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|------------------|-------------------|---------|------------------------------------|
|
||||
| `ChildContent` | `RenderFragment` | `null` | Text or content inside the button |
|
||||
| `OnClick` | `EventCallback` | — | Fired when the button is clicked |
|
||||
| `CooldownSeconds`| `int` | `12` | Duration of the cooldown in seconds|
|
||||
| `Size` | `int` | `120` | Width and height in pixels (square)|
|
||||
|
||||
### Timer Loop
|
||||
|
||||
A `System.Timers.Timer` fires every ~33 ms (≈30 fps) during the cooldown:
|
||||
|
||||
```
|
||||
OnTick:
|
||||
elapsed = UtcNow - startTime
|
||||
if elapsed >= CooldownSeconds → end cooldown, dispose timer
|
||||
_elapsedAngle = (elapsed / CooldownSeconds) * 360
|
||||
_remainingSeconds = CooldownSeconds - (int)elapsed
|
||||
InvokeAsync(StateHasChanged)
|
||||
```
|
||||
|
||||
On each tick, `_elapsedAngle` is written into the overlay's inline `style` attribute, causing Blazor to re-render the `mask-image`. The timer is disposed in `Dispose()` to prevent leaks.
|
||||
|
||||
### Disposal
|
||||
|
||||
The component implements `IDisposable` to clean up the timer when the component is removed from the render tree. This follows the same pattern used by `SearchDialogComponent` and `BuildChartComponent` elsewhere in the codebase.
|
||||
|
||||
---
|
||||
|
||||
## CSS Masking Details
|
||||
|
||||
Two vendor-prefixed properties are set to ensure cross-browser support:
|
||||
|
||||
```
|
||||
mask-image: conic-gradient(...);
|
||||
-webkit-mask-image: conic-gradient(...);
|
||||
```
|
||||
|
||||
The overlay uses `pointer-events: none` and `user-select: none` so that interaction passes through to the button underneath (which is disabled and transparent).
|
||||
|
||||
An `rgba(22, 22, 24, 0.82)` semi-transparent background on the overlay produces the greyed-out appearance. The mask controls *where* this background is visible.
|
||||
|
||||
---
|
||||
|
||||
## Usage on the Home Page
|
||||
|
||||
The component is added to `Pages/Pages/Home/HomePage.razor` inside the first `PaperComponent`:
|
||||
|
||||
```razor
|
||||
<CooldownButtonComponent CooldownSeconds="12"
|
||||
Size="120"
|
||||
OnClick="OnCooldownClick">
|
||||
Click Me
|
||||
</CooldownButtonComponent>
|
||||
```
|
||||
|
||||
The `OnCooldownClick` handler in the page's `@code` block currently returns `Task.CompletedTask` (a no-op). This is the extension point where real work (e.g. triggering a game action, calling an API, showing a toast) would go.
|
||||
|
||||
---
|
||||
|
||||
## Adapting the Component
|
||||
|
||||
- **Change cooldown duration** – set `CooldownSeconds` to any positive integer.
|
||||
- **Change button size** – set `Size` to any pixel dimension (button remains square).
|
||||
- **Custom content** – pass any Blazor markup as `ChildContent` (text, icons, spinners).
|
||||
- **Handle the click** – attach a handler to `OnClick` that returns `Task` or `void`.
|
||||
|
||||
The `--cooldown-size` CSS custom property is set inline on the wrapper so that the label, overlay, and button all scale together.
|
||||
Reference in New Issue
Block a user