fix(aria): replace plain Refresh button with canonical circular auto-refresh control
CD - Develop / build-and-deploy (push) Failing after 13m3s
CD - Develop / build-and-deploy (push) Failing after 13m3s
All three list pages (ThreatEvents, PrecursorAlerts, DevicePosture) now use the template's btn-manual-refresh icon button + SVG countdown timer in header-controls, matching the canonical TablePage.svelte pattern. Plain text Refresh button removed from toolbar-right. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { ariaApi, type DeviceSecurityPosture, type OsEolStatus } from '../lib/api';
|
import { ariaApi, type DeviceSecurityPosture, type OsEolStatus } from '../lib/api';
|
||||||
import { addToast } from '../lib/stores';
|
import { addToast } from '../lib/stores';
|
||||||
import Pagination from './common/Pagination.svelte';
|
import Pagination from './common/Pagination.svelte';
|
||||||
@@ -88,7 +88,42 @@
|
|||||||
return !!(d.goldImageHash && d.currentImageHash && d.goldImageHash !== d.currentImageHash);
|
return !!(d.goldImageHash && d.currentImageHash && d.goldImageHash !== d.currentImageHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => load(0));
|
// Auto-refresh
|
||||||
|
const REFRESH_SECONDS = 60;
|
||||||
|
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||||
|
let secondsLeft = REFRESH_SECONDS;
|
||||||
|
let refreshing = false;
|
||||||
|
let autoRefreshEnabled = true;
|
||||||
|
let tickInterval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||||
|
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
autoRefreshEnabled = !autoRefreshEnabled;
|
||||||
|
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRefresh() {
|
||||||
|
refreshing = true;
|
||||||
|
await load(currentPage);
|
||||||
|
refreshing = false;
|
||||||
|
secondsLeft = REFRESH_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load(0);
|
||||||
|
tickInterval = setInterval(() => {
|
||||||
|
if (!autoRefreshEnabled) return;
|
||||||
|
if (secondsLeft > 1) {
|
||||||
|
secondsLeft -= 1;
|
||||||
|
} else {
|
||||||
|
secondsLeft = REFRESH_SECONDS;
|
||||||
|
doRefresh();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => clearInterval(tickInterval));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
@@ -98,6 +133,34 @@
|
|||||||
<h1>Device Posture</h1>
|
<h1>Device Posture</h1>
|
||||||
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
|
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-controls">
|
||||||
|
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||||
|
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||||
|
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||||
|
on:click={toggleAutoRefresh}
|
||||||
|
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||||
|
aria-label="Toggle auto-refresh">
|
||||||
|
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||||
|
{#if autoRefreshEnabled}
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none"
|
||||||
|
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||||
|
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||||
|
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||||
|
{:else}
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||||
|
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||||
|
{/if}
|
||||||
|
{#if refreshing}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||||
|
{:else if autoRefreshEnabled}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||||
|
{:else}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
@@ -179,9 +242,6 @@
|
|||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="toolbar-count">{totalElements} device{totalElements !== 1 ? 's' : ''}</span>
|
<span class="toolbar-count">{totalElements} device{totalElements !== 1 ? 's' : ''}</span>
|
||||||
<div class="toolbar-right">
|
|
||||||
<button class="btn-refresh" on:click={() => load(currentPage)}>Refresh</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
@@ -462,12 +522,25 @@
|
|||||||
}
|
}
|
||||||
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-shrink: 0; }
|
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-shrink: 0; }
|
||||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||||
.toolbar-right { display: flex; gap: 0.5rem; }
|
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||||
.btn-refresh {
|
.btn-manual-refresh {
|
||||||
background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
|
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||||
border-radius: 6px; padding: 6px 12px; font-size: var(--font-size-body-sm); font-weight: 500; cursor: pointer;
|
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.1rem; cursor: pointer; transition: background 0.15s; flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.btn-refresh:hover { background: #e5e7eb; }
|
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||||
|
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.refresh-countdown {
|
||||||
|
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||||
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||||
|
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||||
|
.refresh-countdown.paused { opacity: 0.5; }
|
||||||
|
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||||
|
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||||
|
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||||
|
|
||||||
/* Table card — canonical */
|
/* Table card — canonical */
|
||||||
.table-card {
|
.table-card {
|
||||||
@@ -590,7 +663,6 @@
|
|||||||
:global(.dark-mode) .filter-tag-remove { background: #374151; color: #e5e7eb; }
|
:global(.dark-mode) .filter-tag-remove { background: #374151; color: #e5e7eb; }
|
||||||
|
|
||||||
:global(.dark-mode) .toolbar-count { color: #9ca3af; }
|
:global(.dark-mode) .toolbar-count { color: #9ca3af; }
|
||||||
:global(.dark-mode) .btn-refresh { background: #374151; color: #e5e7eb; border-color: #4b5563; }
|
|
||||||
|
|
||||||
:global(.dark-mode) .table-card { background: #1f2937; }
|
:global(.dark-mode) .table-card { background: #1f2937; }
|
||||||
:global(.dark-mode) thead { background: #111827; border-bottom-color: #374151; }
|
:global(.dark-mode) thead { background: #111827; border-bottom-color: #374151; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { ariaApi, type PrecursorSequenceAlert, type SequenceType, type ThreatEvent } from '../lib/api';
|
import { ariaApi, type PrecursorSequenceAlert, type SequenceType, type ThreatEvent } from '../lib/api';
|
||||||
import { addToast } from '../lib/stores';
|
import { addToast } from '../lib/stores';
|
||||||
import Pagination from './common/Pagination.svelte';
|
import Pagination from './common/Pagination.svelte';
|
||||||
@@ -108,7 +108,42 @@
|
|||||||
return Math.round(c * 100) + '%';
|
return Math.round(c * 100) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => load(0));
|
// Auto-refresh
|
||||||
|
const REFRESH_SECONDS = 60;
|
||||||
|
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||||
|
let secondsLeft = REFRESH_SECONDS;
|
||||||
|
let refreshing = false;
|
||||||
|
let autoRefreshEnabled = true;
|
||||||
|
let tickInterval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||||
|
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
autoRefreshEnabled = !autoRefreshEnabled;
|
||||||
|
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRefresh() {
|
||||||
|
refreshing = true;
|
||||||
|
await load(currentPage);
|
||||||
|
refreshing = false;
|
||||||
|
secondsLeft = REFRESH_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load(0);
|
||||||
|
tickInterval = setInterval(() => {
|
||||||
|
if (!autoRefreshEnabled) return;
|
||||||
|
if (secondsLeft > 1) {
|
||||||
|
secondsLeft -= 1;
|
||||||
|
} else {
|
||||||
|
secondsLeft = REFRESH_SECONDS;
|
||||||
|
doRefresh();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => clearInterval(tickInterval));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
@@ -126,6 +161,34 @@
|
|||||||
<h1>Precursor Alerts</h1>
|
<h1>Precursor Alerts</h1>
|
||||||
<p>Multi-phase attack sequence detection — jackpotting and skimming precursors</p>
|
<p>Multi-phase attack sequence detection — jackpotting and skimming precursors</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-controls">
|
||||||
|
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||||
|
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||||
|
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||||
|
on:click={toggleAutoRefresh}
|
||||||
|
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||||
|
aria-label="Toggle auto-refresh">
|
||||||
|
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||||
|
{#if autoRefreshEnabled}
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none"
|
||||||
|
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||||
|
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||||
|
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||||
|
{:else}
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||||
|
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||||
|
{/if}
|
||||||
|
{#if refreshing}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||||
|
{:else if autoRefreshEnabled}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||||
|
{:else}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
@@ -220,9 +283,6 @@
|
|||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="toolbar-count">{totalElements} alert{totalElements !== 1 ? 's' : ''}</span>
|
<span class="toolbar-count">{totalElements} alert{totalElements !== 1 ? 's' : ''}</span>
|
||||||
<div class="toolbar-right">
|
|
||||||
<button class="btn-refresh" on:click={() => load(currentPage)}>Refresh</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
@@ -488,12 +548,26 @@
|
|||||||
}
|
}
|
||||||
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-shrink: 0; }
|
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-shrink: 0; }
|
||||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||||
.toolbar-right { display: flex; gap: 0.5rem; }
|
|
||||||
.btn-refresh {
|
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||||
background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
|
.btn-manual-refresh {
|
||||||
border-radius: 6px; padding: 6px 12px; font-size: var(--font-size-body-sm); font-weight: 500; cursor: pointer;
|
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.1rem; cursor: pointer; transition: background 0.15s; flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.btn-refresh:hover { background: #e5e7eb; }
|
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||||
|
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.refresh-countdown {
|
||||||
|
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||||
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||||
|
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||||
|
.refresh-countdown.paused { opacity: 0.5; }
|
||||||
|
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||||
|
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||||
|
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||||
|
|
||||||
/* Table card — canonical */
|
/* Table card — canonical */
|
||||||
.table-card {
|
.table-card {
|
||||||
@@ -631,7 +705,6 @@
|
|||||||
:global(.dark-mode) .filter-tag-remove { background: #374151; color: #e5e7eb; }
|
:global(.dark-mode) .filter-tag-remove { background: #374151; color: #e5e7eb; }
|
||||||
|
|
||||||
:global(.dark-mode) .toolbar-count { color: #9ca3af; }
|
:global(.dark-mode) .toolbar-count { color: #9ca3af; }
|
||||||
:global(.dark-mode) .btn-refresh { background: #374151; color: #e5e7eb; border-color: #4b5563; }
|
|
||||||
|
|
||||||
:global(.dark-mode) .table-card { background: #1f2937; }
|
:global(.dark-mode) .table-card { background: #1f2937; }
|
||||||
:global(.dark-mode) thead { background: #111827; border-bottom-color: #374151; }
|
:global(.dark-mode) thead { background: #111827; border-bottom-color: #374151; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { ariaApi, type ThreatEvent, type ThreatSeverity } from '../lib/api';
|
import { ariaApi, type ThreatEvent, type ThreatSeverity } from '../lib/api';
|
||||||
import { addToast } from '../lib/stores';
|
import { addToast } from '../lib/stores';
|
||||||
import Pagination from './common/Pagination.svelte';
|
import Pagination from './common/Pagination.svelte';
|
||||||
@@ -89,7 +89,42 @@
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
// Auto-refresh
|
||||||
|
const REFRESH_SECONDS = 60;
|
||||||
|
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||||
|
let secondsLeft = REFRESH_SECONDS;
|
||||||
|
let refreshing = false;
|
||||||
|
let autoRefreshEnabled = true;
|
||||||
|
let tickInterval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||||
|
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
autoRefreshEnabled = !autoRefreshEnabled;
|
||||||
|
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRefresh() {
|
||||||
|
refreshing = true;
|
||||||
|
await load();
|
||||||
|
refreshing = false;
|
||||||
|
secondsLeft = REFRESH_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load();
|
||||||
|
tickInterval = setInterval(() => {
|
||||||
|
if (!autoRefreshEnabled) return;
|
||||||
|
if (secondsLeft > 1) {
|
||||||
|
secondsLeft -= 1;
|
||||||
|
} else {
|
||||||
|
secondsLeft = REFRESH_SECONDS;
|
||||||
|
doRefresh();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => clearInterval(tickInterval));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page-wrap">
|
<div class="page-wrap">
|
||||||
@@ -98,6 +133,34 @@
|
|||||||
<h1>Threat Events</h1>
|
<h1>Threat Events</h1>
|
||||||
<p>IOC match events detected across the fleet</p>
|
<p>IOC match events detected across the fleet</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-controls">
|
||||||
|
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||||
|
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||||
|
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||||
|
on:click={toggleAutoRefresh}
|
||||||
|
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||||
|
aria-label="Toggle auto-refresh">
|
||||||
|
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||||
|
{#if autoRefreshEnabled}
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none"
|
||||||
|
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||||
|
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||||
|
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||||
|
{:else}
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||||
|
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||||
|
{/if}
|
||||||
|
{#if refreshing}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||||
|
{:else if autoRefreshEnabled}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||||
|
{:else}
|
||||||
|
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
@@ -229,6 +292,26 @@
|
|||||||
padding: 24px 24px 0;
|
padding: 24px 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||||
|
.btn-manual-refresh {
|
||||||
|
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.1rem; cursor: pointer; transition: background 0.15s; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||||
|
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.refresh-countdown {
|
||||||
|
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||||
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||||
|
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||||
|
.refresh-countdown.paused { opacity: 0.5; }
|
||||||
|
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||||
|
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||||
|
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||||
|
|
||||||
.page-body {
|
.page-body {
|
||||||
display: flex; flex: 1; overflow: hidden; min-height: 0;
|
display: flex; flex: 1; overflow: hidden; min-height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user