feat: add auto-refresh toggle to TopBar and GlobalFilterProvider
Some checks failed
Build & Publish / publish (push) Failing after 5s

Add autoRefresh/setAutoRefresh to GlobalFilterContext, persisted in
localStorage. TopBar shows a LIVE/PAUSED toggle button with pulsing
dot indicator. Consumers can use useGlobalFilters().autoRefresh to
conditionally enable/disable polling intervals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-24 17:58:20 +01:00
parent 039f2fa5fe
commit 795ffef9dc
4 changed files with 87 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@cameleer/design-system", "name": "@cameleer/design-system",
"version": "0.1.0", "version": "0.1.2",
"type": "module", "type": "module",
"main": "./dist/index.es.js", "main": "./dist/index.es.js",
"module": "./dist/index.es.js", "module": "./dist/index.es.js",
@@ -12,8 +12,12 @@
}, },
"./style.css": "./dist/style.css" "./style.css": "./dist/style.css"
}, },
"files": ["dist"], "files": [
"sideEffects": ["*.css"], "dist"
],
"sideEffects": [
"*.css"
],
"publishConfig": { "publishConfig": {
"registry": "https://gitea.siegeln.net/api/packages/cameleer/npm/" "registry": "https://gitea.siegeln.net/api/packages/cameleer/npm/"
}, },
@@ -27,7 +31,8 @@
"build:lib": "vite build --config vite.lib.config.ts", "build:lib": "vite build --config vite.lib.config.ts",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest" "test": "vitest",
"test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
"react": "^19.0.0", "react": "^19.0.0",
@@ -40,6 +45,7 @@
"react-router-dom": "^7.0.0" "react-router-dom": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",

View File

@@ -81,6 +81,56 @@
flex-shrink: 0; flex-shrink: 0;
} }
.liveToggle {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-raised);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.5px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
height: 30px;
}
.liveToggle:hover {
border-color: var(--text-faint);
}
.liveToggleActive {
color: var(--success);
border-color: var(--success-border);
background: var(--success-bg);
}
.liveToggleActive:hover {
border-color: var(--success);
}
.liveDot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-muted);
flex-shrink: 0;
}
.liveToggleActive .liveDot {
background: var(--success);
animation: livePulse 2s ease-in-out infinite;
}
@keyframes livePulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.themeToggle { .themeToggle {
background: none; background: none;
border: 1px solid var(--border); border: 1px solid var(--border);

View File

@@ -84,8 +84,18 @@ export function TopBar({
onChange={globalFilters.setTimeRange} onChange={globalFilters.setTimeRange}
/> />
{/* Right: theme toggle, env badge, user */} {/* Right: auto-refresh toggle, theme toggle, env badge, user */}
<div className={styles.right}> <div className={styles.right}>
<button
className={`${styles.liveToggle} ${globalFilters.autoRefresh ? styles.liveToggleActive : ''}`}
onClick={() => globalFilters.setAutoRefresh(!globalFilters.autoRefresh)}
type="button"
aria-label={globalFilters.autoRefresh ? 'Disable auto-refresh' : 'Enable auto-refresh'}
title={globalFilters.autoRefresh ? 'Auto-refresh is on — click to pause' : 'Auto-refresh is paused — click to resume'}
>
<span className={styles.liveDot} />
{globalFilters.autoRefresh ? 'LIVE' : 'PAUSED'}
</button>
<button <button
className={styles.themeToggle} className={styles.themeToggle}
onClick={toggleTheme} onClick={toggleTheme}

View File

@@ -16,6 +16,8 @@ interface GlobalFilterContextValue {
toggleStatus: (status: ExchangeStatus) => void toggleStatus: (status: ExchangeStatus) => void
clearStatusFilters: () => void clearStatusFilters: () => void
isInTimeRange: (timestamp: Date) => boolean isInTimeRange: (timestamp: Date) => boolean
autoRefresh: boolean
setAutoRefresh: (enabled: boolean) => void
} }
const GlobalFilterContext = createContext<GlobalFilterContextValue | null>(null) const GlobalFilterContext = createContext<GlobalFilterContextValue | null>(null)
@@ -27,9 +29,17 @@ function getDefaultTimeRange(): TimeRange {
return { start, end, preset: DEFAULT_PRESET } return { start, end, preset: DEFAULT_PRESET }
} }
function getInitialAutoRefresh(): boolean {
try {
const stored = localStorage.getItem('cameleer:auto-refresh')
return stored === null ? true : stored === 'true'
} catch { return true }
}
export function GlobalFilterProvider({ children }: { children: ReactNode }) { export function GlobalFilterProvider({ children }: { children: ReactNode }) {
const [timeRange, setTimeRangeState] = useState<TimeRange>(getDefaultTimeRange) const [timeRange, setTimeRangeState] = useState<TimeRange>(getDefaultTimeRange)
const [statusFilters, setStatusFilters] = useState<Set<ExchangeStatus>>(new Set()) const [statusFilters, setStatusFilters] = useState<Set<ExchangeStatus>>(new Set())
const [autoRefresh, setAutoRefreshState] = useState<boolean>(getInitialAutoRefresh)
const setTimeRange = useCallback((range: TimeRange) => { const setTimeRange = useCallback((range: TimeRange) => {
setTimeRangeState(range) setTimeRangeState(range)
@@ -51,6 +61,11 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) {
setStatusFilters(new Set()) setStatusFilters(new Set())
}, []) }, [])
const setAutoRefresh = useCallback((enabled: boolean) => {
setAutoRefreshState(enabled)
try { localStorage.setItem('cameleer:auto-refresh', String(enabled)) } catch {}
}, [])
const isInTimeRange = useCallback( const isInTimeRange = useCallback(
(timestamp: Date) => { (timestamp: Date) => {
if (timeRange.preset) { if (timeRange.preset) {
@@ -65,7 +80,7 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) {
return ( return (
<GlobalFilterContext.Provider <GlobalFilterContext.Provider
value={{ timeRange, setTimeRange, statusFilters, toggleStatus, clearStatusFilters, isInTimeRange }} value={{ timeRange, setTimeRange, statusFilters, toggleStatus, clearStatusFilters, isInTimeRange, autoRefresh, setAutoRefresh }}
> >
{children} {children}
</GlobalFilterContext.Provider> </GlobalFilterContext.Provider>