Attention cues review fixes (#238)
- NEW lane: Gate A notice pinned below the rows (it scrolled off with >3 entries, same rule as the rail, #46); rows at text-xs for the wall. - Mobile chip moved off MapView's centred "Live" chip, which it covered. - Keyword cues re-resolve to the incident once correlation links the call, so the lane row is clickable and the pin pulses. - Enabling notifications sends one confirmation notification; a browser that grants permission but throws on construct (mobile Chrome) now shows "unsupported" instead of a switched-on control that never delivers. - useActiveIncidentsState: includeMetadataChanges so `loaded` flips on a metadata-only server sync (quiet org / offline start) instead of waiting for the first incident change and swallowing it into the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
eb0faf2aea
commit
292c4fffd9
@@ -173,6 +173,20 @@ export function AttentionControls({ prefs, onChange, armed }: Props) {
|
||||
if (p === "default") {
|
||||
try { p = (await Notification.requestPermission()) as NotifyState; } catch { p = notifyState(); }
|
||||
}
|
||||
if (p === "granted") {
|
||||
// Confirm with one notification (as the tone confirms with a beep). It
|
||||
// doubles as the support check: mobile Chrome grants permission but
|
||||
// throws on `new Notification()` without a service worker, which would
|
||||
// otherwise leave a switched-on control that never delivers anything.
|
||||
try {
|
||||
new Notification("DRB · Notifications on", {
|
||||
body: `Cues will appear here while the live wall tab is hidden.\n${MACHINE_OUTPUT_NOTICE_TEXT}`,
|
||||
tag: "drb-attn:on",
|
||||
});
|
||||
} catch {
|
||||
p = "unsupported";
|
||||
}
|
||||
}
|
||||
setPerm(p);
|
||||
if (p === "granted") onChange({ ...prefs, notify: true });
|
||||
};
|
||||
@@ -215,7 +229,7 @@ export function AttentionControls({ prefs, onChange, armed }: Props) {
|
||||
<span>
|
||||
<span className="text-ink font-medium">Browser notifications while this tab is hidden</span>
|
||||
{perm === "denied" && <span className="block text-ink-muted">Blocked in this browser's site settings — allow notifications there first.</span>}
|
||||
{perm === "unsupported" && <span className="block text-ink-muted">This browser doesn't support page notifications.</span>}
|
||||
{perm === "unsupported" && <span className="block text-ink-muted">This browser doesn't show notifications from a web page (mobile browsers often only allow them from installed apps).</span>}
|
||||
</span>
|
||||
</label>
|
||||
<div>
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
*
|
||||
* Desktop: overlay at the map's top-left, right of the zoom stack and above
|
||||
* MapView's rail (the rail starts at top-[9.5rem], so the lane is capped
|
||||
* just short of it and scrolls). Mobile: a compact chip that expands.
|
||||
* just short of it and its rows scroll). The Gate A notice is pinned below
|
||||
* the rows, never inside the scroll area — same rule as the rail (#46).
|
||||
* Mobile: a compact chip under MapView's centred "Live" chip (same top-3
|
||||
* centre slot would cover it), right of the fit-all button; it expands.
|
||||
* Hidden when there is nothing new.
|
||||
*/
|
||||
import { useState } from "react";
|
||||
@@ -31,7 +34,7 @@ function Row({ e, now, onSelect }: { e: LaneEntry; now: number; onSelect: (id: s
|
||||
<span className="shrink-0 tabular-nums text-ink-muted">{ago(e.at, now)}</span>
|
||||
</>
|
||||
);
|
||||
const cls = "w-full flex items-center gap-1.5 px-1.5 py-1 rounded text-[11px] text-left";
|
||||
const cls = "w-full flex items-center gap-1.5 px-1.5 py-1 rounded text-xs text-left";
|
||||
return (
|
||||
<li>
|
||||
{e.incidentId ? (
|
||||
@@ -53,28 +56,30 @@ export function NewLane({ entries, now, onSelect }: { entries: LaneEntry[]; now:
|
||||
return (
|
||||
<>
|
||||
<section aria-label="New in the last 15 minutes"
|
||||
className="absolute top-3 left-14 z-[1002] hidden md:flex flex-col w-56 max-h-[8.25rem] bg-surface/90 backdrop-blur-sm border border-accent/60 rounded-lg shadow pointer-events-auto">
|
||||
className="absolute top-3 left-14 z-[1002] hidden md:flex flex-col w-56 max-h-[8.5rem] bg-surface/90 backdrop-blur-sm border border-accent/60 rounded-lg shadow pointer-events-auto">
|
||||
<h2 className="shrink-0 px-2 pt-1.5 text-[10px] font-semibold uppercase tracking-wide text-accent">
|
||||
New · last 15 min <span className="text-ink-muted font-normal">({entries.length})</span>
|
||||
</h2>
|
||||
<ul className="min-h-0 overflow-y-auto px-0.5 pb-1">
|
||||
<ul className="min-h-0 overflow-y-auto px-0.5">
|
||||
{entries.map((e) => <Row key={e.key} e={e} now={now} onSelect={onSelect} />)}
|
||||
<li>{notice}</li>
|
||||
</ul>
|
||||
<div className="shrink-0 border-t border-line pb-1">{notice}</div>
|
||||
</section>
|
||||
|
||||
<div className="absolute top-3 left-1/2 -translate-x-1/2 z-[1002] md:hidden flex flex-col items-center">
|
||||
<div className="absolute top-[5.25rem] left-14 z-[1002] md:hidden flex flex-col items-start">
|
||||
<button type="button" aria-expanded={open} onClick={() => setOpen((v) => !v)}
|
||||
className="bg-surface/95 border border-accent/60 rounded-full px-3 py-1 text-xs text-accent font-semibold whitespace-nowrap shadow">
|
||||
● {entries.length} new · 15 min {open ? "▴" : "▾"}
|
||||
</button>
|
||||
{open && (
|
||||
<ul className="mt-1 w-64 max-w-[calc(100vw-2rem)] max-h-48 overflow-y-auto bg-surface/95 border border-line rounded-lg shadow p-0.5">
|
||||
{entries.map((e) => (
|
||||
<Row key={e.key} e={e} now={now} onSelect={(id) => { setOpen(false); onSelect(id); }} />
|
||||
))}
|
||||
<li>{notice}</li>
|
||||
</ul>
|
||||
<div className="mt-1 w-64 max-w-[calc(100vw-4.5rem)] bg-surface/95 border border-line rounded-lg shadow p-0.5 flex flex-col">
|
||||
<ul className="max-h-40 overflow-y-auto">
|
||||
{entries.map((e) => (
|
||||
<Row key={e.key} e={e} now={now} onSelect={(id) => { setOpen(false); onSelect(id); }} />
|
||||
))}
|
||||
</ul>
|
||||
<div className="shrink-0 border-t border-line pb-1">{notice}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user