108 lines
2.8 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|