Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import OsmlTray from "@/components/OsmlTray";
import StatusDisplay from "@/components/StatusDisplay";
import Logo from "@/components/Logo";
import FeaturePopup, { type FeaturePopupData } from "@/components/FeaturePopup";
import ConfigWarnings from "@/components/alert/ConfigWarnings";
import { ResourceProvider } from "@/context/ResourceContext";

/** Natural Earth II fallback (offline, bundled with Cesium) */
Expand Down Expand Up @@ -81,6 +82,8 @@ const App = () => {
if (!baseLayer) return null;

return (
<>
<ConfigWarnings />
<ResiumViewer
ref={viewerRef}
full
Expand Down Expand Up @@ -113,6 +116,7 @@ const App = () => {
)}
</ResourceProvider>
</ResiumViewer>
</>
);
};

Expand Down
124 changes: 124 additions & 0 deletions src/components/alert/ConfigWarnings.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/* Copyright 2023-2026 Amazon.com, Inc. or its affiliates. */


/* ── ConfigWarnings toast stack ─────────────────────────────────── */

.cw-stack {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 3000;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
max-width: 520px;
width: 90vw;
}

/* ── Individual toast ──────────────────────────────────────────── */

.cw-toast {
pointer-events: auto;
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
animation: cw-slide-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) both;
}

.cw-toast--exit {
animation: cw-slide-out 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

/* ── Content ───────────────────────────────────────────────────── */

.cw-toast-body {
flex: 1;
min-width: 0;
}

.cw-toast-title {
font-size: 13px;
font-weight: 650;
margin-bottom: 2px;
}

.cw-toast-message {
font-size: 12px;
line-height: 1.45;
opacity: 0.72;
}

/* ── Action button (e.g. Retry) ────────────────────────────────── */

.cw-toast-action {
flex-shrink: 0;
background: rgba(220, 38, 38, 0.2);
border: 1px solid rgba(220, 38, 38, 0.3);
border-radius: 8px;
color: #fca5a5;
padding: 6px 14px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease;
white-space: nowrap;
align-self: center;
}

.cw-toast-action:hover {
background: rgba(220, 38, 38, 0.35);
}

.cw-toast-action:disabled {
opacity: 0.5;
cursor: default;
}

/* ── Close button ──────────────────────────────────────────────── */

.cw-toast-close {
flex-shrink: 0;
background: none;
border: none;
padding: 2px;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.15s ease;
margin-top: 1px;
}

.cw-toast-close:hover {
opacity: 1;
}

/* ── Animations ────────────────────────────────────────────────── */

@keyframes cw-slide-in {
from {
opacity: 0;
transform: translateY(-12px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

@keyframes cw-slide-out {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-8px) scale(0.96);
}
}
177 changes: 177 additions & 0 deletions src/components/alert/ConfigWarnings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright 2023-2026 Amazon.com, Inc. or its affiliates.

import { useEffect, useState } from "react";
import { getConfigWarnings, type ConfigWarning } from "@/config";
import "./ConfigWarnings.css";

/** Auto-dismiss delay in ms per severity */
const DISMISS_DELAY: Record<ConfigWarning["severity"], number> = {
warning: 12_000,
error: 20_000
};

const SEVERITY_STYLES: Record<
ConfigWarning["severity"],
{ accent: string; bg: string; border: string; icon: string }
> = {
error: {
accent: "#fca5a5",
bg: "rgba(220, 38, 38, 0.12)",
border: "rgba(220, 38, 38, 0.3)",
icon: "#f87171"
},
warning: {
accent: "#fcd34d",
bg: "rgba(234, 179, 8, 0.10)",
border: "rgba(234, 179, 8, 0.3)",
icon: "#facc15"
}
};

function WarningIcon({ color }: { color: string }) {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
style={{ flexShrink: 0 }}
>
<path
d="M10 2L1 18h18L10 2z"
stroke={color}
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M10 8v4"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
/>
<circle cx="10" cy="15" r="0.8" fill={color} />
</svg>
);
}

function ErrorIcon({ color }: { color: string }) {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
style={{ flexShrink: 0 }}
>
<circle cx="10" cy="10" r="8.5" stroke={color} strokeWidth="1.5" />
<path
d="M10 6v5"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
/>
<circle cx="10" cy="14" r="0.8" fill={color} />
</svg>
);
}

function Toast({
warning,
onDismiss
}: {
warning: ConfigWarning;
onDismiss: () => void;
}) {
const [exiting, setExiting] = useState(false);
const s = SEVERITY_STYLES[warning.severity];

useEffect(() => {
const timer = setTimeout(() => {
setExiting(true);
}, DISMISS_DELAY[warning.severity]);
return () => clearTimeout(timer);
}, [warning.severity]);

// After the exit animation finishes, remove the toast
useEffect(() => {
if (!exiting) return;
const timer = setTimeout(onDismiss, 300);
return () => clearTimeout(timer);
}, [exiting, onDismiss]);

const handleDismiss = () => {
setExiting(true);
};

return (
<div
className={`cw-toast ${exiting ? "cw-toast--exit" : ""}`}
style={{
background: s.bg,
borderColor: s.border
}}
>
{warning.severity === "error" ? (
<ErrorIcon color={s.icon} />
) : (
<WarningIcon color={s.icon} />
)}

<div className="cw-toast-body">
<div className="cw-toast-title" style={{ color: s.accent }}>
{warning.title}
</div>
<div
className="cw-toast-message"
style={{ color: s.accent }}
>
{warning.message}
</div>
</div>

<button
className="cw-toast-close"
onClick={handleDismiss}
style={{ color: s.accent }}
aria-label="Dismiss"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path
d="M3 3l8 8M11 3l-8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
</button>
</div>
);
}

const ConfigWarnings = () => {
const [warnings, setWarnings] = useState<ConfigWarning[]>([]);

useEffect(() => {
let cancelled = false;
getConfigWarnings().then((result) => {
if (!cancelled) setWarnings(result);
});
return () => { cancelled = true; };
}, []);

const dismiss = (index: number) => {
setWarnings((prev) => prev.filter((_, i) => i !== index));
};

if (warnings.length === 0) return null;

return (
<div className="cw-stack">
{warnings.map((w, i) => (
<Toast key={`${w.severity}-${w.title}`} warning={w} onDismiss={() => dismiss(i)} />
))}
</div>
);
};

export default ConfigWarnings;
Loading