rspace-online/shared/components/rstack-offline-indicator.ts

108 lines
2.8 KiB
TypeScript

/**
* <rstack-offline-indicator> — shows connection status in the shell header.
*
* States:
* - online: hidden (no visual noise)
* - syncing: brief spinner (shown during init)
* - offline: orange dot + "Offline"
* - error: red dot + tooltip with error info
*/
import type { RuntimeStatus } from '../local-first/runtime';
export class RStackOfflineIndicator extends HTMLElement {
#shadow: ShadowRoot;
#status: RuntimeStatus = 'idle';
#unsub: (() => void) | null = null;
constructor() {
super();
this.#shadow = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.#render();
// Connect to runtime when available
const tryConnect = () => {
const runtime = (window as any).__rspaceOfflineRuntime;
if (runtime) {
this.#status = runtime.status;
this.#render();
this.#unsub = runtime.onStatusChange((s: RuntimeStatus) => {
this.#status = s;
this.#render();
});
} else {
// Runtime not ready yet, try again shortly
setTimeout(tryConnect, 500);
}
};
tryConnect();
}
disconnectedCallback() {
this.#unsub?.();
this.#unsub = null;
}
#render() {
const hidden = this.#status === 'online' || this.#status === 'idle';
const isOffline = this.#status === 'offline';
const isError = this.#status === 'error';
const isSyncing = this.#status === 'initializing';
const dotColor = isError ? '#ef4444' : isOffline ? '#f59e0b' : '#3b82f6';
const label = isError ? 'Sync Error' : isOffline ? 'Offline' : isSyncing ? 'Syncing' : '';
const tooltip = isError
? 'Unable to connect to sync server. Changes saved locally.'
: isOffline
? 'Working offline. Changes will sync when reconnected.'
: isSyncing
? 'Connecting to sync server...'
: '';
this.#shadow.innerHTML = `
<style>
:host { display: inline-flex; align-items: center; gap: 6px; }
.indicator {
display: ${hidden ? 'none' : 'inline-flex'};
align-items: center;
gap: 5px;
padding: 3px 8px;
border-radius: 12px;
background: var(--rs-bg-secondary, rgba(255,255,255,0.06));
font-size: 11px;
color: var(--rs-text-secondary, #999);
cursor: default;
user-select: none;
}
.dot {
width: 7px; height: 7px;
border-radius: 50%;
background: ${dotColor};
flex-shrink: 0;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.dot.syncing {
animation: pulse 1.2s ease-in-out infinite;
background: #3b82f6;
}
</style>
<div class="indicator" title="${tooltip}">
<span class="dot${isSyncing ? ' syncing' : ''}"></span>
<span>${label}</span>
</div>
`;
}
static define() {
if (!customElements.get('rstack-offline-indicator')) {
customElements.define('rstack-offline-indicator', RStackOfflineIndicator);
}
}
}