Commit b47e6601 by Mac Stephens

Improve mobile reliability, map controls, and WebSocket recovery

parent ab0baace
...@@ -87,7 +87,7 @@ object FMain: TFMain ...@@ -87,7 +87,7 @@ object FMain: TFMain
Top = 18 Top = 18
Width = 141 Width = 141
Height = 25 Height = 25
Caption = 'Disconnect Selected Client' Caption = 'Refresh Client'
TabOrder = 0 TabOrder = 0
OnClick = btnDisconnectClientClick OnClick = btnDisconnectClientClick
end end
......
[Settings] [Settings]
LogFileNum=153 LogFileNum=155
webClientVersion=0.9.4.1 webClientVersion=0.9.4.1
[Database] [Database]
......
...@@ -39,6 +39,8 @@ type ...@@ -39,6 +39,8 @@ type
FSelectProc: TSelectProc; FSelectProc: TSelectProc;
FLoading: Boolean; FLoading: Boolean;
FFirstLoad: Boolean; FFirstLoad: Boolean;
FRefreshPending: Boolean;
FPendingWsData: TJSObject;
[async] procedure GetComplaints; [async] procedure GetComplaints;
procedure HandleListClick(e: TJSMouseEvent); procedure HandleListClick(e: TJSMouseEvent);
procedure ShowHideBusinessRows; procedure ShowHideBusinessRows;
...@@ -59,6 +61,8 @@ procedure TFViewComplaints.WebFormCreate(Sender: TObject); ...@@ -59,6 +61,8 @@ procedure TFViewComplaints.WebFormCreate(Sender: TObject);
begin begin
Document.addEventListener('click', @HandleListClick); Document.addEventListener('click', @HandleListClick);
FFirstLoad := True; FFirstLoad := True;
FRefreshPending := False;
FPendingWsData := nil;
GetComplaints; GetComplaints;
asm asm
if (!window.showComplaintDetails) { if (!window.showComplaintDetails) {
...@@ -178,12 +182,32 @@ begin ...@@ -178,12 +182,32 @@ begin
HideSpinner('spinner'); HideSpinner('spinner');
FFirstLoad := False; FFirstLoad := False;
end; end;
if Assigned(FPendingWsData) then
begin
respObj := FPendingWsData;
FPendingWsData := nil;
ApplyWsData(respObj);
end;
if FRefreshPending then
begin
FRefreshPending := False;
GetComplaints;
end;
end; end;
end; end;
procedure TFViewComplaints.RefreshData; procedure TFViewComplaints.RefreshData;
begin begin
Console.Log('Complaints.RefreshData'); Console.Log('Complaints.RefreshData');
if FLoading then
begin
FRefreshPending := True;
Exit;
end;
GetComplaints; GetComplaints;
end; end;
...@@ -192,7 +216,10 @@ var ...@@ -192,7 +216,10 @@ var
complaintsCount: Integer; complaintsCount: Integer;
begin begin
if FLoading then if FLoading then
begin
FPendingWsData := aRespObj;
Exit; Exit;
end;
xdwdsComplaints.Close; xdwdsComplaints.Close;
xdwdsComplaints.SetJsonData(aRespObj['data']); xdwdsComplaints.SetJsonData(aRespObj['data']);
......
...@@ -11,12 +11,31 @@ ...@@ -11,12 +11,31 @@
<span id="lbl_main_title" class="navbar-brand text-light mb-0 ms-1"></span> <span id="lbl_main_title" class="navbar-brand text-light mb-0 ms-1"></span>
</div> </div>
<!-- Right: Connection / Logout --> <!-- Right: Connection / Menu -->
<div class="d-flex align-items-center gap-2 ms-auto"> <div class="d-flex align-items-center gap-2 ms-auto">
<span id="view.main.lblconnection" class="navbar-text text-light small"></span> <span id="view.main.lblconnection"
<span id="view.main.version" class="navbar-text text-light small opacity-75"></span> class="connection-status connection-status-connecting"
<button id="btn_logout" type="button" class="btn btn-outline-light btn-sm">Logout</button> role="status"
aria-live="polite"
aria-atomic="true"
aria-label="Live updates: Connecting"
title="Live updates: Connecting">
<span class="connection-status-dot" aria-hidden="true"></span>
<span id="view.main.lblconnectiontext" class="connection-status-text">Connecting</span>
</span>
<div class="dropdown">
<button type="button"
class="btn btn-outline-light btn-sm"
data-bs-toggle="dropdown"
aria-expanded="false"
aria-label="Menu">
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><button id="btn_logout" type="button" class="dropdown-item">Logout</button></li>
</ul>
</div>
</div> </div>
</div> </div>
</nav> </nav>
......
...@@ -54,7 +54,11 @@ type ...@@ -54,7 +54,11 @@ type
FDetailsForm: TWebForm; FDetailsForm: TWebForm;
FArchiveForm: TWebForm; FArchiveForm: TWebForm;
FLogoutProc: TLogoutProc; FLogoutProc: TLogoutProc;
FBadgeRefreshInProgress: Boolean;
FBadgeRefreshPending: Boolean;
FPendingWsBadgeCounts: TJSObject;
[async] procedure RefreshBadgesAsync; [async] procedure RefreshBadgesAsync;
procedure ApplyBadgeCounts(aData: TJSObject);
procedure ShowUnitDetails(UnitId: string); procedure ShowUnitDetails(UnitId: string);
procedure SetHeaderTitle(const title: string); procedure SetHeaderTitle(const title: string);
procedure HideDetailsModal; procedure HideDetailsModal;
...@@ -68,6 +72,10 @@ type ...@@ -68,6 +72,10 @@ type
procedure HandleWsComplaintMap(aData: TJSObject); procedure HandleWsComplaintMap(aData: TJSObject);
procedure HandleWsUnitList(aData: TJSObject); procedure HandleWsUnitList(aData: TJSObject);
procedure HandleWsComplaintList(aData: TJSObject); procedure HandleWsComplaintList(aData: TJSObject);
procedure HandleWsConnectionState(AState: TWsConnectionState);
procedure HandleWsRecoveryRequired;
procedure HandleWsAuthenticationExpired;
procedure ResyncLiveData;
type TActivePanel = (apNone, apMap, apUnits, apComplaints); type TActivePanel = (apNone, apMap, apUnits, apComplaints);
var var
...@@ -84,6 +92,7 @@ type ...@@ -84,6 +92,7 @@ type
public public
{ Public declarations } { Public declarations }
destructor Destroy; override;
class procedure Display(LogoutProc: TLogoutProc); class procedure Display(LogoutProc: TLogoutProc);
procedure ShowForm( AFormClass: TWebFormClass ); procedure ShowForm( AFormClass: TWebFormClass );
procedure ShowComplaintDetails(ComplaintId: string); procedure ShowComplaintDetails(ComplaintId: string);
...@@ -121,15 +130,10 @@ const ...@@ -121,15 +130,10 @@ const
procedure TFViewMain.WebFormCreate(Sender: TObject); procedure TFViewMain.WebFormCreate(Sender: TObject);
var var
userName: string; userName: string;
el: TJSElement;
begin begin
userName := JS.toString(AuthService.TokenPayload.Properties['user_name']); userName := JS.toString(AuthService.TokenPayload.Properties['user_name']);
lblUsername.Caption := ' ' + userName.ToLower + ' '; lblUsername.Caption := ' ' + userName.ToLower + ' ';
el := Document.getElementById('view.main.version');
if Assigned(el) then
TJSHtmlElement(el).innerText := 'v' + TDMConnection.clientVersion;
FChildForm := nil; FChildForm := nil;
FDetailsForm := nil; FDetailsForm := nil;
FArchiveForm := nil; FArchiveForm := nil;
...@@ -139,6 +143,9 @@ begin ...@@ -139,6 +143,9 @@ begin
FMapRefreshTick := 0; FMapRefreshTick := 0;
FUnitsRefreshTick := 0; FUnitsRefreshTick := 0;
FComplaintsRefreshTick := 0; FComplaintsRefreshTick := 0;
FBadgeRefreshInProgress := False;
FBadgeRefreshPending := False;
FPendingWsBadgeCounts := nil;
if (not (JS.toBoolean(AuthService.TokenPayload.Properties['user_admin']))) then if (not (JS.toBoolean(AuthService.TokenPayload.Properties['user_admin']))) then
lblUsers.Visible := false; lblUsers.Visible := false;
...@@ -175,9 +182,27 @@ begin ...@@ -175,9 +182,27 @@ begin
dmWebsocket.OnComplaintMap := HandleWsComplaintMap; dmWebsocket.OnComplaintMap := HandleWsComplaintMap;
dmWebsocket.OnUnitList := HandleWsUnitList; dmWebsocket.OnUnitList := HandleWsUnitList;
dmWebsocket.OnComplaintList := HandleWsComplaintList; dmWebsocket.OnComplaintList := HandleWsComplaintList;
dmWebsocket.OnStateChanged := HandleWsConnectionState;
dmWebsocket.OnRecoveryRequired := HandleWsRecoveryRequired;
dmWebsocket.OnAuthenticationExpired := HandleWsAuthenticationExpired;
dmWebsocket.Connect(DMConnection.WsUrl); dmWebsocket.Connect(DMConnection.WsUrl);
end; end;
destructor TFViewMain.Destroy;
begin
if Assigned(dmWebsocket) and (dmWebsocket.Owner = Self) then
begin
dmWebsocket.Stop;
dmWebsocket.Free;
dmWebsocket := nil;
end;
if FViewMain = Self then
FViewMain := nil;
inherited;
end;
procedure TFViewMain.SetActivePanel(panel: TActivePanel); procedure TFViewMain.SetActivePanel(panel: TActivePanel);
begin begin
FActivePanel := panel; FActivePanel := panel;
...@@ -468,10 +493,13 @@ end; ...@@ -468,10 +493,13 @@ end;
// WebSocket push handlers // WebSocket push handlers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TFViewMain.HandleWsBadgeCounts(aData: TJSObject); procedure TFViewMain.ApplyBadgeCounts(aData: TJSObject);
var var
el: TJSElement; el: TJSElement;
begin begin
if not Assigned(aData) then
Exit;
el := Document.getElementById('view.main.badgecomplaints'); el := Document.getElementById('view.main.badgecomplaints');
if Assigned(el) then if Assigned(el) then
TJSHtmlElement(el).innerText := string(aData['BadgeComplaints']); TJSHtmlElement(el).innerText := string(aData['BadgeComplaints']);
...@@ -481,6 +509,17 @@ begin ...@@ -481,6 +509,17 @@ begin
TJSHtmlElement(el).innerText := string(aData['BadgeUnits']); TJSHtmlElement(el).innerText := string(aData['BadgeUnits']);
end; end;
procedure TFViewMain.HandleWsBadgeCounts(aData: TJSObject);
begin
if FBadgeRefreshInProgress then
begin
FPendingWsBadgeCounts := aData;
Exit;
end;
ApplyBadgeCounts(aData);
end;
procedure TFViewMain.HandleWsUnitMap(aData: TJSObject); procedure TFViewMain.HandleWsUnitMap(aData: TJSObject);
begin begin
if Assigned(FMapForm) then if Assigned(FMapForm) then
...@@ -505,6 +544,83 @@ begin ...@@ -505,6 +544,83 @@ begin
FComplaintsForm.ApplyWsData(aData); FComplaintsForm.ApplyWsData(aData);
end; end;
procedure TFViewMain.HandleWsConnectionState(AState: TWsConnectionState);
var
statusElement: TJSHTMLElement;
textElement: TJSHTMLElement;
statusText: string;
statusClass: string;
begin
case AState of
wcsConnecting:
begin
statusText := 'Connecting';
statusClass := 'connecting';
end;
wcsConnected:
begin
statusText := 'Connected';
statusClass := 'connected';
end;
wcsReconnecting:
begin
statusText := 'Reconnecting';
statusClass := 'reconnecting';
end;
wcsOffline:
begin
statusText := 'Offline';
statusClass := 'offline';
end;
else
begin
statusText := 'Disconnected';
statusClass := 'stopped';
end;
end;
statusElement := TJSHTMLElement(Document.getElementById('view.main.lblconnection'));
if Assigned(statusElement) then
begin
statusElement.setAttribute('class', 'connection-status connection-status-' + statusClass);
statusElement.setAttribute('aria-label', 'Live updates: ' + statusText);
statusElement.setAttribute('title', 'Live updates: ' + statusText);
end;
textElement := TJSHTMLElement(Document.getElementById('view.main.lblconnectiontext'));
if Assigned(textElement) then
textElement.innerText := statusText;
end;
procedure TFViewMain.HandleWsRecoveryRequired;
begin
ResyncLiveData;
end;
procedure TFViewMain.HandleWsAuthenticationExpired;
begin
if Assigned(dmWebsocket) then
dmWebsocket.Stop;
if Assigned(FLogoutProc) then
FLogoutProc('Your session has expired. Please sign in again.');
end;
procedure TFViewMain.ResyncLiveData;
begin
if (not AuthService.Authenticated) or AuthService.TokenExpired then
begin
HandleWsAuthenticationExpired;
Exit;
end;
Console.Log('WS: resynchronizing live data');
RefreshBadgesAsync;
Inc(FGlobalRefreshTick);
RefreshActivePanelFromTimer;
end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TFViewMain.tmrBadgeCountsTimer(Sender: TObject); procedure TFViewMain.tmrBadgeCountsTimer(Sender: TObject);
...@@ -528,25 +644,52 @@ var ...@@ -528,25 +644,52 @@ var
badgeObj: TJSObject; badgeObj: TJSObject;
el: TJSElement; el: TJSElement;
begin begin
if FBadgeRefreshInProgress then
begin
FBadgeRefreshPending := True;
Exit;
end;
FBadgeRefreshInProgress := True;
try try
resp := await(xdwcBadgeCounts.RawInvokeAsync('IApiService.GetBadgeCounts', [])); try
badgeObj := TJSObject(resp.Result); resp := await(xdwcBadgeCounts.RawInvokeAsync('IApiService.GetBadgeCounts', []));
badgeObj := TJSObject(resp.Result);
el := Document.getElementById('view.main.badgecomplaints');
if Assigned(el) then if Assigned(FPendingWsBadgeCounts) then
TJSHtmlElement(el).innerText := string(badgeObj['BadgeComplaints']); begin
ApplyBadgeCounts(FPendingWsBadgeCounts);
el := Document.getElementById('view.main.badgeunits'); FPendingWsBadgeCounts := nil;
if Assigned(el) then end
TJSHtmlElement(el).innerText := string(badgeObj['BadgeUnits']); else
except ApplyBadgeCounts(badgeObj);
on E: Exception do except
on E: Exception do
begin
if Assigned(FPendingWsBadgeCounts) then
begin
ApplyBadgeCounts(FPendingWsBadgeCounts);
FPendingWsBadgeCounts := nil;
end
else
begin
el := Document.getElementById('view.main.badgecomplaints');
if Assigned(el) then TJSHtmlElement(el).innerText := '�';
el := Document.getElementById('view.main.badgeunits');
if Assigned(el) then TJSHtmlElement(el).innerText := '�';
end;
Console.Log('Badge refresh error: ' + E.Message);
end;
end;
finally
FBadgeRefreshInProgress := False;
if FBadgeRefreshPending then
begin begin
el := Document.getElementById('view.main.badgecomplaints'); FBadgeRefreshPending := False;
if Assigned(el) then TJSHtmlElement(el).innerText := '�'; RefreshBadgesAsync;
el := Document.getElementById('view.main.badgeunits');
if Assigned(el) then TJSHtmlElement(el).innerText := '�';
Console.Log('Badge refresh error: ' + E.Message);
end; end;
end; end;
end; end;
......
...@@ -62,7 +62,6 @@ object FViewMap: TFViewMap ...@@ -62,7 +62,6 @@ object FViewMap: TFViewMap
end end
object httpReqGeoJson: TWebHttpRequest object httpReqGeoJson: TWebHttpRequest
ResponseType = rtText ResponseType = rtText
URL = 'assets/orleanscounty.geojson'
OnResponse = httpReqGeoJsonResponse OnResponse = httpReqGeoJsonResponse
Left = 116 Left = 116
Top = 698 Top = 698
......
...@@ -52,16 +52,17 @@ ...@@ -52,16 +52,17 @@
<i class="fa fa-crosshairs"></i> <i class="fa fa-crosshairs"></i>
</button> </button>
<!-- Filters (top-right) --> <!-- Filters (bottom-right, above recenter) -->
<button id="btn_map_filters" <button id="btn_map_filters"
type="button" type="button"
class="btn btn-primary position-absolute top-0 end-0 m-2 shadow" class="btn btn-primary position-absolute end-0 me-2 shadow"
style="z-index:1000;" style="z-index:1000; bottom:4.5rem;"
data-bs-toggle="offcanvas" data-bs-toggle="offcanvas"
data-bs-target="#map_filters_offcanvas" data-bs-target="#map_filters_offcanvas"
aria-controls="map_filters_offcanvas"> aria-controls="map_filters_offcanvas"
<i class="fa fa-sliders-h"></i> aria-label="Map filters"
<span class="d-none d-sm-inline">Filters</span> title="Map filters">
<i class="fa fa-sliders-h" aria-hidden="true"></i>
</button> </button>
</div> </div>
</div> </div>
......
...@@ -31,6 +31,7 @@ type ...@@ -31,6 +31,7 @@ type
FUnitsLoaded: Boolean; FUnitsLoaded: Boolean;
FComplaintsLoaded: Boolean; FComplaintsLoaded: Boolean;
FLoadingPoints: Boolean; FLoadingPoints: Boolean;
FRefreshPending: Boolean;
mapFilters: TMapFilters; mapFilters: TMapFilters;
FPendingUnitId: string; FPendingUnitId: string;
FPendingComplaintId: string; FPendingComplaintId: string;
...@@ -293,10 +294,12 @@ begin ...@@ -293,10 +294,12 @@ begin
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', [])); resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result); root := TJSObject(resp.Result);
unitsData := TJSArray(root['data']); unitsData := TJSArray(root['data']);
FUnitsLoaded := True; FUnitsLoaded := Assigned(unitsData);
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage); Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
on E: Exception do
Console.Log('Units error: ' + E.Message);
end; end;
// --- Fetch Complaints ---------------------------------------------------- // --- Fetch Complaints ----------------------------------------------------
...@@ -304,10 +307,12 @@ begin ...@@ -304,10 +307,12 @@ begin
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', [])); resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
root := TJSObject(resp.Result); root := TJSObject(resp.Result);
complaintsData := TJSArray(root['data']); complaintsData := TJSArray(root['data']);
FComplaintsLoaded := True; FComplaintsLoaded := Assigned(complaintsData);
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage); Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
on E: Exception do
Console.Log('Complaints error: ' + E.Message);
end; end;
// A WebSocket snapshot received while the HTTP requests were in flight is // A WebSocket snapshot received while the HTTP requests were in flight is
...@@ -329,8 +334,10 @@ begin ...@@ -329,8 +334,10 @@ begin
// --- Place markers (BeginUpdate wraps both so the map redraws once) ------ // --- Place markers (BeginUpdate wraps both so the map redraws once) ------
lfMap.BeginUpdate; lfMap.BeginUpdate;
try try
PlaceUnitMarkers(unitsData); if FUnitsLoaded then
PlaceComplaintMarkers(complaintsData); PlaceUnitMarkers(unitsData);
if FComplaintsLoaded then
PlaceComplaintMarkers(complaintsData);
finally finally
lfMap.EndUpdate; lfMap.EndUpdate;
end; end;
...@@ -344,6 +351,12 @@ begin ...@@ -344,6 +351,12 @@ begin
if showBusy then if showBusy then
HideSpinner('spinner'); HideSpinner('spinner');
FLoadingPoints := False; FLoadingPoints := False;
if FRefreshPending then
begin
FRefreshPending := False;
LoadPointsAsync(False);
end;
end; end;
end; end;
...@@ -716,22 +729,17 @@ begin ...@@ -716,22 +729,17 @@ begin
end; end;
procedure TFViewMap.btnFindLocationClick(Sender: TObject); procedure TFViewMap.btnFindLocationClick(Sender: TObject);
var
coord: TTMSFNCMapsCoordinateRec;
begin begin
if userLocationMarker = nil then tmrLocate.Enabled := False;
Exit; FDoFocusZoom := False;
coord := CreateCoordinate(userLocationMarker.Latitude, userLocationMarker.Longitude);
lfMap.SetCenterCoordinate(coord);
FPendingFocusCoord := coord;
FPendingFocusZoom := 17;
FDoFocusZoom := True;
FPendingFocusMarkerData := ''; FPendingFocusMarkerData := '';
FPendingUnitId := '';
FPendingComplaintId := '';
tmrLocate.Interval := 250; if lfMap.Polygons.Count = 0 then
tmrLocate.Enabled := True; Exit;
lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray);
end; end;
...@@ -855,6 +863,13 @@ end; ...@@ -855,6 +863,13 @@ end;
procedure TFViewMap.RefreshData; procedure TFViewMap.RefreshData;
begin begin
Console.Log('Map.RefreshData'); Console.Log('Map.RefreshData');
if FLoadingPoints then
begin
FRefreshPending := True;
Exit;
end;
LoadPointsAsync(False); LoadPointsAsync(False);
end; end;
......
...@@ -37,6 +37,8 @@ type ...@@ -37,6 +37,8 @@ type
private private
FLoading: Boolean; FLoading: Boolean;
FFirstLoad: Boolean; FFirstLoad: Boolean;
FRefreshPending: Boolean;
FPendingWsData: TJSObject;
[async] procedure GetUnits; [async] procedure GetUnits;
procedure HandleListClick(e: TJSMouseEvent); procedure HandleListClick(e: TJSMouseEvent);
public public
...@@ -57,6 +59,8 @@ begin ...@@ -57,6 +59,8 @@ begin
DMConnection.ApiConnection.Connected := True; DMConnection.ApiConnection.Connected := True;
Document.addEventListener('click', @HandleListClick); Document.addEventListener('click', @HandleListClick);
FFirstLoad := True; FFirstLoad := True;
FRefreshPending := False;
FPendingWsData := nil;
GetUnits; GetUnits;
asm asm
...@@ -158,12 +162,32 @@ begin ...@@ -158,12 +162,32 @@ begin
FFirstLoad := False; FFirstLoad := False;
FLoading := False; FLoading := False;
if Assigned(FPendingWsData) then
begin
respObj := FPendingWsData;
FPendingWsData := nil;
ApplyWsData(respObj);
end;
if FRefreshPending then
begin
FRefreshPending := False;
GetUnits;
end;
end; end;
end; end;
procedure TFViewUnits.RefreshData; procedure TFViewUnits.RefreshData;
begin begin
Console.Log('Units.RefreshData'); Console.Log('Units.RefreshData');
if FLoading then
begin
FRefreshPending := True;
Exit;
end;
GetUnits; GetUnits;
end; end;
...@@ -172,7 +196,10 @@ var ...@@ -172,7 +196,10 @@ var
unitCount: Integer; unitCount: Integer;
begin begin
if FLoading then if FLoading then
begin
FPendingWsData := aRespObj;
Exit; Exit;
end;
xdwdsUnits.Close; xdwdsUnits.Close;
xdwdsUnits.SetJsonData(aRespObj['data']); xdwdsUnits.SetJsonData(aRespObj['data']);
......
...@@ -82,6 +82,70 @@ html, body { ...@@ -82,6 +82,70 @@ html, body {
.tab-hidden { display: none !important; } .tab-hidden { display: none !important; }
.connection-status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 1.75rem;
padding: 0.2rem 0.55rem;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 999px;
color: #fff;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.connection-status-dot {
width: 0.55rem;
height: 0.55rem;
flex: 0 0 auto;
border-radius: 50%;
background-color: #adb5bd;
}
.connection-status-connected .connection-status-dot {
background-color: #75d995;
box-shadow: 0 0 0 0.14rem rgba(117, 217, 149, 0.2);
}
.connection-status-connecting .connection-status-dot,
.connection-status-reconnecting .connection-status-dot {
background-color: #ffd166;
animation: connection-status-pulse 1.4s ease-in-out infinite;
}
.connection-status-offline .connection-status-dot,
.connection-status-stopped .connection-status-dot {
background-color: #ff8b94;
}
@keyframes connection-status-pulse {
0%, 100% { opacity: 0.45; }
50% { opacity: 1; }
}
@media (max-width: 430px) {
.connection-status {
width: 1.75rem;
justify-content: center;
padding-right: 0;
padding-left: 0;
}
.connection-status-text {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.connection-status-connecting .connection-status-dot,
.connection-status-reconnecting .connection-status-dot {
animation: none;
}
}
.summary-chevron-icon { .summary-chevron-icon {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment