mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 13:48:31 +00:00
fix: improve the status page
This commit is contained in:
@@ -4,33 +4,50 @@ const timestamp = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', time
|
||||
|
||||
export function IncidentTimeline({ updates }: { updates: PublicIncidentUpdate[] }) {
|
||||
return (
|
||||
<section className="incident-timeline-section" aria-labelledby="incident-updates-title">
|
||||
<header className="incident-timeline-heading">
|
||||
<section className="pt-6.75" aria-labelledby="incident-updates-title">
|
||||
<header className="flex items-end justify-between gap-4.5">
|
||||
<div>
|
||||
<p className="overline">Activity</p>
|
||||
<h2 id="incident-updates-title">Incident updates</h2>
|
||||
<p className="overline mb-1 dark:text-brand-soft">Activity</p>
|
||||
<h2 id="incident-updates-title" className="m-0 text-lg font-medium leading-entry tracking-tight-sm dark:text-neutral-50">
|
||||
Incident updates
|
||||
</h2>
|
||||
</div>
|
||||
<span>
|
||||
<span className="text-footnote text-faint dark:text-faint">
|
||||
{updates.length} {updates.length === 1 ? 'update' : 'updates'}
|
||||
</span>
|
||||
</header>
|
||||
{updates.length > 0 ? (
|
||||
<ol className="incident-timeline">
|
||||
<ol className="m-0 mt-5.5 grid list-none gap-0 p-0">
|
||||
{updates.map((update, index) => (
|
||||
<li key={`${update.createdAt}-${index}`}>
|
||||
<span className="incident-timeline-dot" aria-hidden="true" />
|
||||
<div className="incident-timeline-entry">
|
||||
<header>
|
||||
<strong>{update.status}</strong>
|
||||
<time dateTime={update.createdAt}>{timestamp.format(new Date(update.createdAt))}</time>
|
||||
<li
|
||||
key={`${update.createdAt}-${index}`}
|
||||
className="relative grid grid-cols-[var(--spacing-timeline-col)_minmax(0,1fr)] gap-3.25 pb-6.25 not-last:before:absolute not-last:before:bottom-0 not-last:before:left-1.25 not-last:before:top-3 not-last:before:w-px not-last:before:bg-border"
|
||||
>
|
||||
<span
|
||||
className="relative z-1 mt-0.75 size-2.75 rounded-full border-3 border-card bg-primary-deep shadow-timeline-dot"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<header className="flex items-baseline justify-between gap-4.5 max-sm:flex-col max-sm:items-start max-sm:gap-0.75">
|
||||
<strong className="text-caption font-semibold capitalize dark:text-neutral-100">{update.status}</strong>
|
||||
<time
|
||||
className="font-mono text-2xs leading-overline text-faint whitespace-nowrap dark:text-faint"
|
||||
dateTime={update.createdAt}
|
||||
>
|
||||
{timestamp.format(new Date(update.createdAt))}
|
||||
</time>
|
||||
</header>
|
||||
<p>{update.body}</p>
|
||||
<p className="m-0 mt-1.75 max-w-dialog-channel text-sm leading-body-relaxed text-ink-detail dark:text-neutral-300">
|
||||
{update.body}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<p className="incident-timeline-empty">No updates have been posted for this incident.</p>
|
||||
<p className="my-5.5 mb-7 rounded-md border border-dashed border-border-dashed-light bg-surface-elevated p-4.5 text-caption text-muted-foreground dark:border-white/10 dark:bg-night-summary dark:text-neutral-400">
|
||||
No updates have been posted for this incident.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -10,10 +10,10 @@ type HistoryEntry = PublicService['history'][number];
|
||||
type HistoryDay = { day: number; uptimePct: number | null | undefined };
|
||||
|
||||
function dayClass(uptimePct: number | null | undefined) {
|
||||
if (uptimePct === null || uptimePct === undefined) return 'is-empty';
|
||||
if (uptimePct === 100) return 'is-up';
|
||||
if (uptimePct === 0) return 'is-down';
|
||||
return 'is-partial';
|
||||
if (uptimePct === null || uptimePct === undefined) return 'bg-uptime-empty dark:bg-night-uptime-empty';
|
||||
if (uptimePct === 100) return 'bg-brand';
|
||||
if (uptimePct === 0) return 'bg-uptime-down dark:bg-red-500';
|
||||
return 'bg-uptime-partial dark:bg-amber-600';
|
||||
}
|
||||
|
||||
function formatDay(day: number) {
|
||||
@@ -56,9 +56,9 @@ export function StatusHistoryBar({ history }: { history: HistoryEntry[] }) {
|
||||
const periods = mobile ? summarizeDays(days) : days.map(({ day, uptimePct }) => ({ startDay: day, endDay: day, uptimePct }));
|
||||
|
||||
return (
|
||||
<div className="status-history">
|
||||
<div className="w-full min-w-0">
|
||||
<div
|
||||
className="uptime-days"
|
||||
className="flex h-8 max-mobile:h-7 w-full items-stretch gap-0.5 max-tablet:gap-px"
|
||||
aria-label={
|
||||
mobile
|
||||
? `Uptime over the last ${HISTORY_DAYS} days, summarized in ${MOBILE_DAYS_PER_BAR}-day periods`
|
||||
@@ -67,7 +67,7 @@ export function StatusHistoryBar({ history }: { history: HistoryEntry[] }) {
|
||||
>
|
||||
{periods.map(({ startDay, endDay, uptimePct }) => (
|
||||
<span
|
||||
className={`uptime-day ${dayClass(uptimePct)}`}
|
||||
className={`min-w-0.5 flex-1 rounded-xs transition-[filter,transform] duration-140 ease-out hover:z-1 hover:scale-y-112 hover:brightness-92 hover:saturate-115 dark:hover:brightness-115 dark:hover:saturate-125 ${dayClass(uptimePct)}`}
|
||||
key={startDay}
|
||||
title={
|
||||
startDay === endDay
|
||||
@@ -79,9 +79,12 @@ export function StatusHistoryBar({ history }: { history: HistoryEntry[] }) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="uptime-days-caption" aria-hidden="true">
|
||||
<div
|
||||
className="mt-2 flex items-center gap-2.25 font-mono text-3xs leading-snug-sm text-ink-caption dark:text-faint"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span>{HISTORY_DAYS} days ago</span>
|
||||
<i />
|
||||
<i className="h-px flex-1 bg-border-row dark:bg-white/8" />
|
||||
<span>Today</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,56 +28,74 @@ export function IncidentDetailPage({ id }: { id: number }) {
|
||||
});
|
||||
return (
|
||||
<div className="status-page-shell">
|
||||
<header className="dashboard-header status-header">
|
||||
<div className="dashboard-header-inner status-header-inner">
|
||||
<header className="relative z-20 border-b border-border-row bg-white/94 backdrop-blur-header dark:border-white/8 dark:bg-night-root/85 dark:backdrop-blur-md">
|
||||
<div className="mx-auto flex h-17 w-[min(var(--spacing-status-max),calc(100%-48px))] max-tablet:w-[min(var(--spacing-status-max),calc(100%-32px))] max-mobile:h-15.5 items-center justify-between">
|
||||
<a className="brand" href="/" aria-label="Upwatch public status">
|
||||
<Zap className="brand-mark" fill="currentColor" /> <span>upwatch</span>
|
||||
</a>
|
||||
<div className="status-header-actions">
|
||||
<div className="flex items-center gap-2 [&_.app-nav-icon]:dark:text-neutral-400 [&_.app-nav-icon:hover:not(:disabled)]:dark:text-brand [&_.app-nav-icon:hover:not(:disabled)]:dark:bg-brand/12">
|
||||
<ThemeToggle className="app-nav-icon" />
|
||||
<Button variant="unstyled" className="status-header-action" onClick={() => navigate('/')}>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="min-h-8.5 max-mobile:min-h-9 px-3.25 max-mobile:px-3.5 py-1.5 rounded-md border border-border-action bg-white/86 text-xs font-medium text-ink-body cursor-pointer transition-[border-color,color,transform] duration-160 ease-out hover:-translate-y-px hover:border-border-accent-hover hover:text-accent-green-hover dark:border-white/12 dark:bg-white/4 dark:text-neutral-300 dark:hover:border-brand/35 dark:hover:bg-brand/8 dark:hover:text-brand"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
Status page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="status-main incident-detail-page">
|
||||
<button className="incident-back" type="button" onClick={() => navigate('/')}>
|
||||
<main className="mx-auto w-[min(var(--spacing-status-max),calc(100%-48px))] max-tablet:w-[min(var(--spacing-status-max),calc(100%-32px))] pt-8.5 max-sm:pt-panel-x pb-14 max-tablet:pb-11">
|
||||
<button
|
||||
className="inline-flex cursor-pointer items-center gap-1.75 border-0 bg-transparent p-0 text-caption text-muted-foreground hover:text-ink [&>svg]:size-3.75 dark:text-neutral-400 dark:hover:text-neutral-50"
|
||||
type="button"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
<ArrowLeft aria-hidden="true" /> All service status
|
||||
</button>
|
||||
{query.isPending ? (
|
||||
<p className="status-loading" aria-busy="true">
|
||||
<p className="mt-10.5 text-caption text-muted-foreground dark:text-neutral-400" aria-busy="true">
|
||||
Loading incident…
|
||||
</p>
|
||||
) : query.isError || !query.data ? (
|
||||
<section className="incident-detail-card incident-detail-error">
|
||||
<section className="mt-panel-x max-sm:mt-5.5 grid place-items-center rounded-card border border-border-card bg-card p-8 py-16 text-center shadow-incident-detail [&>svg]:size-6 [&>svg]:text-danger-detail [&>h1]:mt-3.5 [&>h1]:text-xl [&>h1]:font-medium [&>p]:mt-1.75 [&>p]:text-caption [&>p]:text-muted-foreground dark:border-red-400/30 dark:bg-night-danger dark:shadow-incident-detail-dark dark:[&>h1]:text-red-100 dark:[&>p]:text-neutral-300">
|
||||
<TriangleAlert aria-hidden="true" />
|
||||
<h1>Incident not found</h1>
|
||||
<p>{query.error?.message ?? 'The requested incident could not be loaded.'}</p>
|
||||
</section>
|
||||
) : (
|
||||
<article className="incident-detail-card">
|
||||
<header className="incident-detail-header">
|
||||
<div className="incident-detail-title">
|
||||
<p className="overline">Incident report</p>
|
||||
<h1>{query.data.incident.title}</h1>
|
||||
<article className="mt-panel-x max-sm:mt-5.5 overflow-hidden rounded-card border border-border-card bg-card p-8 pb-2 max-sm:p-5 max-sm:pb-1.25 shadow-incident-detail dark:border-white/8 dark:bg-night-panel dark:shadow-incident-detail-dark">
|
||||
<header className="flex items-center justify-between gap-4.5 max-sm:flex-col max-sm:items-start max-sm:gap-3.25 *:data-[slot=badge]:shrink-0 *:data-[slot=badge]:capitalize">
|
||||
<div className="min-w-0">
|
||||
<p className="overline mb-1.75 text-brand-deep dark:text-brand-soft">Incident report</p>
|
||||
<h1 className="m-0 text-incident-title max-sm:text-2xl font-medium leading-tight-md tracking-incident-title dark:text-neutral-50">
|
||||
{query.data.incident.title}
|
||||
</h1>
|
||||
</div>
|
||||
<Badge variant={query.data.incident.status === 'resolved' ? 'online' : 'offline'}>{query.data.incident.status}</Badge>
|
||||
</header>
|
||||
<dl className="incident-detail-summary">
|
||||
<div>
|
||||
<dt>Started</dt>
|
||||
<dd>
|
||||
<dl className="mt-7 grid grid-cols-[minmax(190px,1.15fr)_minmax(100px,0.6fr)_minmax(190px,1fr)] max-sm:grid-cols-1 -mx-8 max-sm:-mx-5 px-8 max-sm:px-5 max-sm:py-1 border-y border-border-summary bg-surface-elevated dark:border-white/7 dark:bg-night-summary">
|
||||
<div className="min-w-0 py-4.25 max-sm:py-3.25 pr-5 max-sm:pr-0 not-first:pl-5 max-sm:not-first:pl-0 not-first:border-l not-first:border-border-summary max-sm:not-first:border-l-0 max-sm:not-first:border-t max-sm:not-first:border-border-row dark:not-first:border-white/7">
|
||||
<dt className="mb-1.5 font-mono text-2xs leading-entry font-medium uppercase tracking-mono-label text-faint dark:text-faint">
|
||||
Started
|
||||
</dt>
|
||||
<dd className="m-0 text-caption font-medium leading-alert text-ink-label-muted dark:text-neutral-200">
|
||||
<time dateTime={query.data.incident.startedAt}>{dateTime.format(new Date(query.data.incident.startedAt))}</time>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Impact</dt>
|
||||
<dd className="incident-detail-impact">{query.data.incident.impact}</dd>
|
||||
<div className="min-w-0 py-4.25 max-sm:py-3.25 pr-5 max-sm:pr-0 not-first:pl-5 max-sm:not-first:pl-0 not-first:border-l not-first:border-border-summary max-sm:not-first:border-l-0 max-sm:not-first:border-t max-sm:not-first:border-border-row dark:not-first:border-white/7">
|
||||
<dt className="mb-1.5 font-mono text-2xs leading-entry font-medium uppercase tracking-mono-label text-faint dark:text-faint">
|
||||
Impact
|
||||
</dt>
|
||||
<dd className="m-0 text-caption font-medium leading-alert text-ink-label-muted capitalize dark:text-neutral-200">
|
||||
{query.data.incident.impact}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Affected services</dt>
|
||||
<dd>
|
||||
<div className="min-w-0 py-4.25 max-sm:py-3.25 pr-5 max-sm:pr-0 not-first:pl-5 max-sm:not-first:pl-0 not-first:border-l not-first:border-border-summary max-sm:not-first:border-l-0 max-sm:not-first:border-t max-sm:not-first:border-border-row dark:not-first:border-white/7">
|
||||
<dt className="mb-1.5 font-mono text-2xs leading-entry font-medium uppercase tracking-mono-label text-faint dark:text-faint">
|
||||
Affected services
|
||||
</dt>
|
||||
<dd className="m-0 text-caption font-medium leading-alert text-ink-label-muted dark:text-neutral-200">
|
||||
{query.data.incident.services.length > 0
|
||||
? query.data.incident.services.map((service) => service.name).join(', ')
|
||||
: 'General service incident'}
|
||||
|
||||
+212
-89
@@ -56,6 +56,33 @@ const INCIDENT_IMPACT: Record<string, { label: string; tone: string }> = {
|
||||
none: { label: 'Maintenance', tone: 'none' },
|
||||
};
|
||||
|
||||
const BANNER_STYLES: Record<PublicOverallStatus, { banner: string; icon: string }> = {
|
||||
operational: {
|
||||
banner:
|
||||
'border-banner-operational-border text-banner-operational-text bg-linear-to-r from-banner-operational-from to-banner-operational-to shadow-banner-operational dark:border-brand/25 dark:text-brand-soft dark:bg-linear-to-r dark:from-brand/12 dark:via-brand-deep/5 dark:to-night-banner-operational/60 dark:shadow-banner-operational-dark',
|
||||
icon: 'border-brand-deep/22 dark:border-brand/30 dark:bg-night-banner-operational/75',
|
||||
},
|
||||
degraded: {
|
||||
banner:
|
||||
'border-banner-degraded-border text-banner-degraded-text bg-linear-to-r from-banner-degraded-from to-banner-degraded-to shadow-banner-degraded dark:border-banner-degraded-dark-border dark:text-banner-degraded-dark-text dark:bg-linear-to-r dark:from-banner-degraded-dark-from dark:via-banner-degraded-dark-via dark:to-banner-degraded-dark-to dark:shadow-banner-degraded-dark',
|
||||
icon: 'border-banner-degraded-icon-border dark:border-banner-degraded-dark-icon-border dark:bg-banner-degraded-dark-icon-bg',
|
||||
},
|
||||
down: {
|
||||
banner:
|
||||
'border-banner-down-border text-banner-down-text bg-linear-to-r from-danger-bg to-banner-down-to shadow-banner-down dark:border-chart-down/35 dark:text-red-400 dark:bg-linear-to-r dark:from-chart-down/14 dark:via-banner-down-dark-via dark:to-banner-down-dark-to dark:shadow-banner-down-dark',
|
||||
icon: 'border-banner-down-icon-border dark:border-chart-down/35 dark:bg-banner-down-dark-icon-bg',
|
||||
},
|
||||
};
|
||||
|
||||
const IMPACT_TONE_STYLES: Record<string, string> = {
|
||||
critical:
|
||||
'border-tone-critical-border bg-tone-critical-bg text-tone-critical-text dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-400',
|
||||
major: 'border-tone-major-border bg-tone-major-bg text-tone-major-text dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400',
|
||||
minor:
|
||||
'border-tone-minor-border bg-tone-minor-bg text-tone-minor-text dark:border-yellow-500/30 dark:bg-yellow-500/10 dark:text-yellow-400',
|
||||
none: 'border-border-card bg-tone-none-bg text-muted-text dark:border-white/12 dark:bg-white/4 dark:text-neutral-400',
|
||||
};
|
||||
|
||||
const maintenanceTime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||
const resolvedTime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
@@ -104,13 +131,13 @@ export function StatusPage() {
|
||||
|
||||
return (
|
||||
<div className="status-page-shell">
|
||||
<header className="dashboard-header status-header">
|
||||
<div className="dashboard-header-inner status-header-inner">
|
||||
<header className="relative z-20 border-b border-border-row bg-white/94 backdrop-blur-header dark:border-white/8 dark:bg-night-root/85 dark:backdrop-blur-md">
|
||||
<div className="mx-auto flex h-17 w-[min(var(--spacing-status-max),calc(100%-48px))] max-tablet:w-[min(var(--spacing-status-max),calc(100%-32px))] max-mobile:h-15.5 items-center justify-between">
|
||||
<a className="brand" href="/" aria-label="Upwatch public status">
|
||||
<Zap className="brand-mark" fill="currentColor" />
|
||||
<span>upwatch</span>
|
||||
</a>
|
||||
<div className="status-header-actions">
|
||||
<div className="flex items-center gap-2 [&_.app-nav-icon]:dark:text-neutral-400 [&_.app-nav-icon:hover:not(:disabled)]:dark:text-brand [&_.app-nav-icon:hover:not(:disabled)]:dark:bg-brand/12">
|
||||
<ThemeToggle className="app-nav-icon" />
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -133,22 +160,29 @@ export function StatusPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="status-main">
|
||||
<section className="status-intro">
|
||||
<p className="overline">System status</p>
|
||||
<h1>Service availability</h1>
|
||||
<p>Live operational health and 90-day availability for every public service.</p>
|
||||
<main className="mx-auto w-[min(var(--spacing-status-max),calc(100%-48px))] max-tablet:w-[min(var(--spacing-status-max),calc(100%-32px))] py-18 pb-14 max-tablet:py-13 max-tablet:pb-11">
|
||||
<section className="max-w-155">
|
||||
<p className="overline dark:text-brand-soft">System status</p>
|
||||
<h1 className="m-0 text-hero-status font-medium leading-hero tracking-hero max-mobile:tracking-hero-mobile dark:text-neutral-50">
|
||||
Service availability
|
||||
</h1>
|
||||
<p className="mt-4.5 max-w-140 text-subtitle leading-body-relaxed text-muted-foreground dark:text-neutral-400">
|
||||
Live operational health and 90-day availability for every public service.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{statusQuery.isPending ? (
|
||||
<div className="status-loading" aria-busy="true" aria-label="Loading service status">
|
||||
<div className="status-banner-skeleton" />
|
||||
<div className="status-service-skeleton">
|
||||
<div className="mt-10.5" aria-busy="true" aria-label="Loading service status">
|
||||
<div className="h-29 rounded-card bg-skeleton-banner bg-size-[220%_100%] animate-skeleton dark:bg-skeleton-banner-dark" />
|
||||
<div className="mt-4.5 overflow-hidden rounded-card border border-border-skeleton bg-white dark:border-white/8 dark:bg-night-panel">
|
||||
{[0, 1, 2].map((item) => (
|
||||
<div key={item}>
|
||||
<i />
|
||||
<span />
|
||||
<b />
|
||||
<div
|
||||
key={item}
|
||||
className="flex flex-wrap items-center gap-4 p-5.5 not-first:border-t not-first:border-border-row dark:not-first:border-white/6"
|
||||
>
|
||||
<i className="block size-10.5 shrink-0 rounded-badge bg-skeleton-item bg-size-[220%_100%] animate-skeleton dark:bg-skeleton-item-dark" />
|
||||
<span className="block h-8.5 w-[min(220px,75%)] rounded-badge bg-skeleton-item bg-size-[220%_100%] animate-skeleton dark:bg-skeleton-item-dark" />
|
||||
<b className="block h-8 w-full rounded-badge bg-skeleton-item bg-size-[220%_100%] animate-skeleton dark:bg-skeleton-item-dark" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -168,47 +202,76 @@ export function StatusPage() {
|
||||
</Empty>
|
||||
) : (
|
||||
<>
|
||||
<section className={`status-banner ${status.overall}`} aria-live="polite">
|
||||
<span className="status-banner-icon">
|
||||
<section
|
||||
className={`mt-10.5 grid min-h-29 grid-cols-[auto_1fr_auto] items-center gap-4.5 rounded-card border p-6 px-panel-x animate-enter max-tablet:grid-cols-[auto_1fr] max-mobile:grid-cols-1 max-mobile:gap-3 max-mobile:p-5 max-mobile:py-5.5 ${BANNER_STYLES[status.overall].banner}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
<span
|
||||
className={`grid size-11.5 max-mobile:size-10 place-items-center rounded-full border bg-white/72 [&>svg]:size-5.25 ${BANNER_STYLES[status.overall].icon}`}
|
||||
>
|
||||
<OverallIcon status={status.overall} />
|
||||
</span>
|
||||
<div>
|
||||
<h2>{OVERALL_COPY[status.overall].title}</h2>
|
||||
<p>{OVERALL_COPY[status.overall].detail}</p>
|
||||
<h2 className="m-0 text-lg font-semibold tracking-card-heading dark:text-neutral-50">
|
||||
{OVERALL_COPY[status.overall].title}
|
||||
</h2>
|
||||
<p className="mt-1.5 text-xs text-inherit opacity-75 dark:opacity-88">{OVERALL_COPY[status.overall].detail}</p>
|
||||
</div>
|
||||
<time dateTime={new Date(status.updatedAt).toISOString()}>{relativeUpdate(status.updatedAt, now)}</time>
|
||||
<time
|
||||
className="justify-self-end font-mono text-2xs leading-overline opacity-66 max-tablet:col-start-2 max-tablet:justify-self-start max-mobile:col-auto dark:opacity-75 whitespace-nowrap"
|
||||
dateTime={new Date(status.updatedAt).toISOString()}
|
||||
>
|
||||
{relativeUpdate(status.updatedAt, now)}
|
||||
</time>
|
||||
</section>
|
||||
|
||||
{maintenanceServices.length > 0 && (
|
||||
<section className="scheduled-maintenance" aria-labelledby="scheduled-maintenance-title" aria-live="polite">
|
||||
<header className="scheduled-maintenance-header">
|
||||
<div className="scheduled-maintenance-heading">
|
||||
<span className="scheduled-maintenance-icon">
|
||||
<section
|
||||
className="mt-4.5 overflow-hidden rounded-card border border-maintenance-card-border bg-white shadow-maintenance-card animate-enter dark:border-maintenance-blue/25 dark:bg-night-card dark:shadow-[0_16px_40px_rgba(0,0,0,0.4)]"
|
||||
aria-labelledby="scheduled-maintenance-title"
|
||||
aria-live="polite"
|
||||
>
|
||||
<header className="flex min-h-23 items-center justify-between gap-6 border-b border-maintenance-header-border bg-linear-to-r from-maintenance-gradient-from via-maintenance-gradient-via to-white p-4.5 px-5.5 max-mobile:flex-col max-mobile:items-start max-mobile:gap-3.5 max-mobile:p-4 dark:border-white/7 dark:bg-linear-to-r dark:from-maintenance-icon/12 dark:via-none dark:to-night-card/80">
|
||||
<div className="flex items-center gap-3.5">
|
||||
<span className="grid size-10 shrink-0 place-items-center rounded-lg border border-maintenance-icon/20 bg-white/82 text-maintenance-icon [&>svg]:size-4.5 dark:border-maintenance-blue/30 dark:bg-maintenance-icon/15 dark:text-maintenance-blue-light">
|
||||
<Wrench aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<p>Scheduled maintenance</p>
|
||||
<h2 id="scheduled-maintenance-title">Planned service work is in progress</h2>
|
||||
<p className="m-0 mb-0.75 font-mono text-3xs font-medium uppercase tracking-overline text-maintenance-overline dark:text-maintenance-overline-dark">
|
||||
Scheduled maintenance
|
||||
</p>
|
||||
<h2
|
||||
id="scheduled-maintenance-title"
|
||||
className="m-0 text-card-title font-semibold tracking-card-heading text-maintenance-title dark:text-maintenance-title-dark"
|
||||
>
|
||||
Planned service work is in progress
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<span className="scheduled-maintenance-count">
|
||||
<span className="rounded-badge border border-maintenance-badge-border bg-white/72 px-2 py-1.25 font-mono text-3xs font-medium text-maintenance-badge-text max-mobile:ml-13.5 dark:border-maintenance-blue/25 dark:bg-maintenance-icon/12 dark:text-maintenance-badge-text-dark">
|
||||
{maintenanceServices.length} {maintenanceServices.length === 1 ? 'service' : 'services'}
|
||||
</span>
|
||||
</header>
|
||||
<div className="scheduled-maintenance-list">
|
||||
<div className="flex flex-col">
|
||||
{maintenanceServices.map((service) => (
|
||||
<article className="scheduled-maintenance-row" key={service.id}>
|
||||
<div className="scheduled-maintenance-service">
|
||||
<span className="scheduled-maintenance-service-icon">
|
||||
<article
|
||||
className="flex min-h-22 items-center justify-between gap-6 p-4.5 px-5.5 not-first:border-t not-first:border-maintenance-row-border max-mobile:flex-col max-mobile:items-start max-mobile:gap-3.5 max-mobile:p-4 dark:not-first:border-white/6"
|
||||
key={service.id}
|
||||
>
|
||||
<div className="flex items-center gap-3.5">
|
||||
<span className="grid size-9 shrink-0 place-items-center rounded-card-sm border border-maintenance-icon-box-border bg-maintenance-icon-box-bg [&>img]:size-5 [&>img]:object-contain dark:border-white/8 dark:bg-night-maintenance-icon">
|
||||
<SiteIcon monitorId={service.id} favicon="public" />
|
||||
</span>
|
||||
<div>
|
||||
<strong>{service.name}</strong>
|
||||
<span>{service.maintenance?.name}</span>
|
||||
<strong className="block text-caption dark:text-neutral-100">{service.name}</strong>
|
||||
<span className="mt-1 block text-xs text-muted-foreground dark:text-neutral-400">
|
||||
{service.maintenance?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="scheduled-maintenance-state">
|
||||
<i /> until {maintenanceTime.format(new Date(service.maintenance!.endsAt))}
|
||||
<span className="font-mono text-2xs font-medium text-maintenance-icon whitespace-nowrap max-mobile:ml-12.5 dark:text-maintenance-blue-light">
|
||||
<i className="mr-1.25 inline-block size-1.75 rounded-full bg-maintenance-blue" /> until{' '}
|
||||
{maintenanceTime.format(new Date(service.maintenance!.endsAt))}
|
||||
</span>
|
||||
</article>
|
||||
))}
|
||||
@@ -217,43 +280,65 @@ export function StatusPage() {
|
||||
)}
|
||||
|
||||
{activeIncidents.length > 0 && (
|
||||
<section className="active-incidents" aria-labelledby="active-incidents-title" aria-live="polite">
|
||||
<header className="active-incidents-header">
|
||||
<div className="active-incidents-heading">
|
||||
<span className="active-incidents-icon">
|
||||
<section
|
||||
className="mt-4.5 overflow-hidden rounded-card border border-active-incident-border bg-white shadow-active-incident animate-enter dark:border-red-400/28 dark:bg-night-danger dark:shadow-[0_16px_40px_rgba(0,0,0,0.4)]"
|
||||
aria-labelledby="active-incidents-title"
|
||||
aria-live="polite"
|
||||
>
|
||||
<header className="flex min-h-23 items-center justify-between gap-6 border-b border-active-incident-header-border bg-linear-to-r from-active-incident-from via-active-incident-via to-white p-4.5 px-5.5 max-mobile:flex-col max-mobile:items-start max-mobile:gap-3.5 max-mobile:p-4 dark:border-red-400/18 dark:bg-linear-to-r dark:from-red-400/12 dark:via-none dark:to-night-danger/80">
|
||||
<div className="flex items-center gap-3.5">
|
||||
<span className="grid size-10 shrink-0 place-items-center rounded-lg border border-red-700/20 bg-white/82 text-tone-critical-text shadow-active-incident-icon [&>svg]:size-4.5 dark:border-red-400/30 dark:bg-red-400/12 dark:text-red-400 dark:shadow-none">
|
||||
<TriangleAlert aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<p>Active incident</p>
|
||||
<h2 id="active-incidents-title">We’re working to restore service</h2>
|
||||
<p className="m-0 mb-0.75 font-mono text-3xs font-medium uppercase tracking-overline text-active-incident-overline dark:text-red-400">
|
||||
Active incident
|
||||
</p>
|
||||
<h2
|
||||
id="active-incidents-title"
|
||||
className="m-0 text-card-title font-semibold tracking-card-heading text-active-incident-title dark:text-red-100"
|
||||
>
|
||||
We’re working to restore service
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<span className="active-incidents-count">
|
||||
<span className="shrink-0 rounded-badge border border-active-incident-badge-border bg-white/72 px-2 py-1.25 font-mono text-3xs font-medium text-active-incident-badge-text max-mobile:ml-13.5 dark:border-red-400/30 dark:bg-red-400/10 dark:text-red-300">
|
||||
{activeIncidents.length} {activeIncidents.length === 1 ? 'incident' : 'incidents'} active
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="active-incident-list">
|
||||
<div className="flex flex-col">
|
||||
{activeIncidents.map((incident) => (
|
||||
<article className="active-incident-row" key={incident.id}>
|
||||
<div className="active-incident-service">
|
||||
<span className="active-incident-service-icon">
|
||||
<article
|
||||
className="grid min-h-skeleton-h grid-cols-[minmax(190px,0.55fr)_minmax(320px,1.25fr)_auto] items-center gap-6 p-5 px-5.5 not-first:border-t not-first:border-active-incident-row-border max-tablet:grid-cols-[minmax(180px,0.55fr)_minmax(0,1fr)] max-tablet:gap-4.5 max-mobile:grid-cols-1 max-mobile:gap-3.5 max-mobile:p-4 dark:not-first:border-white/6"
|
||||
key={incident.id}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="grid size-9 shrink-0 place-items-center rounded-card-sm border border-active-incident-icon-border bg-active-incident-icon-bg text-active-incident-icon-text [&>svg]:size-4.25 [&>img]:size-5 [&>img]:rounded-xs [&>img]:object-contain dark:border-red-400/22 dark:bg-red-400/8 dark:text-red-400">
|
||||
<TriangleAlert />
|
||||
</span>
|
||||
<div>
|
||||
<button type="button" onClick={() => navigate(`/incidents/${incident.id}`)}>
|
||||
<strong>{incident.title}</strong>
|
||||
<div className="min-w-0">
|
||||
<button
|
||||
className="cursor-pointer border-0 bg-transparent p-0 text-left"
|
||||
type="button"
|
||||
onClick={() => navigate(`/incidents/${incident.id}`)}
|
||||
>
|
||||
<strong className="block truncate text-sm font-semibold text-ink-heading-dark hover:underline dark:text-neutral-50">
|
||||
{incident.title}
|
||||
</strong>
|
||||
</button>
|
||||
<span>
|
||||
<span className="mt-0.75 block truncate text-2xs text-active-incident-subtext dark:text-neutral-400">
|
||||
{incident.services.length
|
||||
? incident.services.map((service) => service.name).join(', ')
|
||||
: 'General service incident'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>{incident.latestUpdate?.body}</p>
|
||||
<span className="active-incident-state">
|
||||
<i /> {incident.status}
|
||||
<p className="m-0 max-w-[66ch] text-xs leading-body-relaxed text-pretty text-active-incident-body max-mobile:pl-12 dark:text-neutral-300">
|
||||
{incident.latestUpdate?.body}
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1.75 justify-self-end whitespace-nowrap font-mono text-3xs font-medium text-active-incident-meta max-tablet:col-start-2 max-tablet:justify-self-start max-mobile:col-auto max-mobile:ml-12 dark:text-red-400">
|
||||
<i className="size-1.5 rounded-full bg-chart-down shadow-pulse-down animate-blink" /> {incident.status}
|
||||
</span>
|
||||
</article>
|
||||
))}
|
||||
@@ -261,15 +346,22 @@ export function StatusPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="public-services-panel" aria-labelledby="public-services-title">
|
||||
<div className="public-services-heading">
|
||||
<section
|
||||
className="mt-4.5 overflow-hidden rounded-card border border-border-subtle bg-white shadow-card-elevated-green animate-enter dark:border-white/8 dark:bg-night-panel dark:shadow-incident-detail-dark"
|
||||
aria-labelledby="public-services-title"
|
||||
>
|
||||
<div className="flex min-h-22 items-center justify-between gap-6 border-b border-border-heading p-4.5 px-5.5 max-mobile:p-4 max-mobile:py-4.25 dark:border-white/7">
|
||||
<div>
|
||||
<h2 id="public-services-title">Services</h2>
|
||||
<p>Availability is calculated from checks collected over the last 90 days.</p>
|
||||
<h2 id="public-services-title" className="m-0 text-lg font-medium dark:text-neutral-50">
|
||||
Services
|
||||
</h2>
|
||||
<p className="mt-1.25 text-xs text-muted-foreground max-mobile:max-w-62.5 dark:text-neutral-400">
|
||||
Availability is calculated from checks collected over the last 90 days.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="icon-button"
|
||||
className="icon-button max-mobile:size-11 max-mobile:shrink-0 dark:border-white/12 dark:bg-white/4 dark:text-neutral-400 hover:not-disabled:dark:border-brand/35 hover:not-disabled:dark:bg-brand/8 hover:not-disabled:dark:text-brand"
|
||||
type="button"
|
||||
onClick={() => void statusQuery.refetch()}
|
||||
disabled={statusQuery.isFetching}
|
||||
@@ -288,24 +380,31 @@ export function StatusPage() {
|
||||
<EmptyDescription>Service health will appear here after monitoring is enabled.</EmptyDescription>
|
||||
</Empty>
|
||||
) : (
|
||||
<div className="public-service-list">
|
||||
<div className="flex flex-col">
|
||||
{status.services.map((service) => {
|
||||
const serviceStatus = SERVICE_STATUS[service.status];
|
||||
return (
|
||||
<article className="public-service-row" key={service.id}>
|
||||
<div className="public-service-header">
|
||||
<div className="public-service-summary">
|
||||
<article
|
||||
className="flex flex-col gap-4 p-5.5 not-first:border-t not-first:border-border-row hover:bg-surface-dialog-header max-mobile:p-4 max-mobile:py-5 dark:bg-transparent dark:not-first:border-white/6 dark:hover:bg-white/2"
|
||||
key={service.id}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4 max-mobile:gap-3">
|
||||
<div className="flex flex-1 items-center gap-3.5 min-w-0 [&_.service-icon]:dark:border-white/8 [&_.service-icon]:dark:bg-night-icon [&_.service-icon]:dark:text-neutral-400">
|
||||
<span className="service-icon">
|
||||
<SiteIcon monitorId={service.id} favicon="public" />
|
||||
</span>
|
||||
<div className="public-service-identity">
|
||||
<strong>{service.name}</strong>
|
||||
<Badge variant={serviceStatus.className}>{serviceStatus.label}</Badge>
|
||||
<div className="flex flex-1 items-center gap-5 min-w-0">
|
||||
<strong className="truncate text-item font-medium min-w-0 dark:text-neutral-50">{service.name}</strong>
|
||||
<Badge className="shrink-0" variant={serviceStatus.className}>
|
||||
{serviceStatus.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="public-service-uptime">
|
||||
<span>90-day uptime</span>
|
||||
<strong>{service.uptime90d === null ? '—' : `${service.uptime90d.toFixed(1)}%`}</strong>
|
||||
<div className="shrink-0 text-right whitespace-nowrap">
|
||||
<span className="block text-2xs text-faint dark:text-neutral-400">90-day uptime</span>
|
||||
<strong className="mt-1.25 block font-mono text-card-title font-medium leading-tight-md tracking-metric-compact max-mobile:text-item dark:text-neutral-100">
|
||||
{service.uptime90d === null ? '—' : `${service.uptime90d.toFixed(1)}%`}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<StatusHistoryBar history={service.history} />
|
||||
@@ -317,18 +416,28 @@ export function StatusPage() {
|
||||
</section>
|
||||
|
||||
{!historyQuery.isPending && !historyQuery.isError && (
|
||||
<section className="past-incidents" aria-labelledby="past-incidents-title">
|
||||
<header className="past-incidents-header">
|
||||
<div className="past-incidents-heading">
|
||||
<span className="past-incidents-icon">
|
||||
<section
|
||||
className="mt-4.5 overflow-hidden rounded-card border border-border-subtle bg-white shadow-card-elevated-green animate-enter dark:border-white/8 dark:bg-night-panel dark:shadow-incident-detail-dark"
|
||||
aria-labelledby="past-incidents-title"
|
||||
>
|
||||
<header className="flex min-h-23 items-center justify-between gap-6 border-b border-past-incidents-header-border bg-linear-to-r from-past-incidents-from via-past-incidents-via to-white p-4.5 px-5.5 max-mobile:flex-col max-mobile:items-start max-mobile:gap-3.5 max-mobile:p-4 dark:border-white/7 dark:bg-linear-to-r dark:from-brand/7 dark:via-none dark:to-night-panel/80">
|
||||
<div className="flex items-center gap-3.5">
|
||||
<span className="grid size-10 shrink-0 place-items-center rounded-lg border border-brand-deep/18 bg-white/82 text-accent-green-hover [&>svg]:size-4.5 dark:border-brand/30 dark:bg-brand/10 dark:text-brand">
|
||||
<History aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<p>Last 30 days</p>
|
||||
<h2 id="past-incidents-title">Past incidents</h2>
|
||||
<p className="m-0 mb-0.75 font-mono text-3xs font-medium uppercase tracking-overline text-past-incidents-accent dark:text-brand-soft">
|
||||
Last 30 days
|
||||
</p>
|
||||
<h2
|
||||
id="past-incidents-title"
|
||||
className="m-0 text-card-title font-semibold tracking-card-heading text-past-incidents-title dark:text-neutral-50"
|
||||
>
|
||||
Past incidents
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<span className="past-incidents-count">
|
||||
<span className="shrink-0 rounded-badge border border-past-incidents-badge-border bg-white/72 px-2 py-1.25 font-mono text-3xs font-medium text-past-incidents-accent max-mobile:ml-13.5 dark:border-brand/25 dark:bg-brand/8 dark:text-brand-soft">
|
||||
{pastIncidents.length === 0 ? 'No incidents' : `${pastIncidents.length} resolved`}
|
||||
</span>
|
||||
</header>
|
||||
@@ -342,31 +451,41 @@ export function StatusPage() {
|
||||
<EmptyDescription>Every monitored service stayed healthy for the full window.</EmptyDescription>
|
||||
</Empty>
|
||||
) : (
|
||||
<div className="past-incident-list">
|
||||
<div className="flex flex-col">
|
||||
{pastIncidents.map((incident) => {
|
||||
const impact = INCIDENT_IMPACT[incident.impact] ?? INCIDENT_IMPACT.none;
|
||||
return (
|
||||
<button
|
||||
className="past-incident-row"
|
||||
className="group grid w-full cursor-pointer grid-cols-[auto_1fr_auto] items-start gap-3.5 border-0 bg-transparent p-4.5 px-5.5 text-left font-inherit not-first:border-t not-first:border-border-row hover:bg-surface-dialog-header focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand-deep/45 max-mobile:p-4 dark:not-first:border-white/6 dark:hover:bg-white/2 dark:focus-visible:outline-brand/50"
|
||||
key={incident.id}
|
||||
type="button"
|
||||
onClick={() => navigate(`/incidents/${incident.id}`)}
|
||||
>
|
||||
<span className="past-incident-icon">
|
||||
<span className="grid size-7.5 shrink-0 place-items-center rounded-full bg-incident-resolved-bg text-accent-green-hover [&>svg]:size-3.75 dark:bg-brand/12 dark:text-brand">
|
||||
<CircleCheck aria-hidden="true" />
|
||||
</span>
|
||||
<span className="past-incident-body">
|
||||
<span className="past-incident-title">
|
||||
<strong>{incident.title}</strong>
|
||||
<span className={`past-incident-impact ${impact.tone}`}>{impact.label}</span>
|
||||
<span className="grid min-w-0 gap-1">
|
||||
<span className="flex flex-wrap items-center gap-2.5">
|
||||
<strong className="text-sm font-semibold text-ink-heading-dark dark:text-neutral-100">
|
||||
{incident.title}
|
||||
</strong>
|
||||
<span
|
||||
className={`rounded-badge border px-1.75 py-0.75 font-mono text-3xs font-medium uppercase tracking-tag ${IMPACT_TONE_STYLES[impact.tone] ?? IMPACT_TONE_STYLES.none}`}
|
||||
>
|
||||
{impact.label}
|
||||
</span>
|
||||
</span>
|
||||
<span className="past-incident-services">
|
||||
<span className="block truncate text-2xs text-muted-foreground dark:text-neutral-400">
|
||||
{incident.services.length
|
||||
? incident.services.map((service) => service.name).join(', ')
|
||||
: 'General service incident'}
|
||||
</span>
|
||||
{incident.latestUpdate?.body && <span className="past-incident-summary">{incident.latestUpdate.body}</span>}
|
||||
<span className="past-incident-meta">
|
||||
{incident.latestUpdate?.body && (
|
||||
<span className="mt-0.5 line-clamp-2 max-tablet:line-clamp-3 max-w-[72ch] text-xs leading-subtitle text-muted-foreground dark:text-neutral-400">
|
||||
{incident.latestUpdate.body}
|
||||
</span>
|
||||
)}
|
||||
<span className="mt-0.5 block font-mono text-2xs leading-overline text-faint dark:text-faint [&>i]:mx-1.5 [&>i]:not-italic [&>i]:opacity-55">
|
||||
<time dateTime={incident.startedAt}>{formatDate(incident.startedAt)}</time>
|
||||
<i aria-hidden="true">·</i> down {formatDuration(incident.durationMs ?? null, incident.startedAt)}
|
||||
{incident.resolvedAt && (
|
||||
@@ -376,7 +495,10 @@ export function StatusPage() {
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className="past-incident-chevron" aria-hidden="true" />
|
||||
<ChevronRight
|
||||
className="size-4 self-center text-arrow-muted transition-colors group-hover:text-arrow-hover max-mobile:hidden dark:text-neutral-600 dark:group-hover:text-neutral-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -388,10 +510,11 @@ export function StatusPage() {
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="status-footer">
|
||||
<footer className="mx-auto flex w-[min(var(--spacing-status-max),calc(100%-48px))] max-tablet:w-[min(var(--spacing-status-max),calc(100%-32px))] items-center justify-between border-t border-border-footer py-7 pb-9 text-footnote text-ink-footer max-mobile:flex-col max-mobile:items-start max-mobile:gap-2 dark:border-white/8 dark:text-faint">
|
||||
<span>Powered by upwatch</span>
|
||||
<span>
|
||||
<i /> Monitoring from Cloudflare's edge
|
||||
<span className="inline-flex items-center gap-1.75">
|
||||
<i className="size-1.5 rounded-full bg-brand-deep shadow-brand-dot dark:bg-brand dark:shadow-brand-dot-dark" /> Monitoring from
|
||||
Cloudflare's edge
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+171
-15
@@ -2,7 +2,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@import 'shadcn/tailwind.css';
|
||||
@import './styles/StatusPage.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@@ -125,6 +124,19 @@
|
||||
linear-gradient(180deg, #090a0c 0%, #0c0e12 68px, #08090b 68px, #08090b 100%);
|
||||
color: #ededed;
|
||||
}
|
||||
|
||||
.status-page-shell {
|
||||
min-height: 100dvh;
|
||||
background:
|
||||
radial-gradient(circle at 12% 22%, rgb(62 207 142 / 0.07), transparent 28rem), linear-gradient(180deg, #fff 0, #fff 36%, #fafcfb 100%);
|
||||
}
|
||||
.dark .status-page-shell {
|
||||
background:
|
||||
radial-gradient(circle at 12% 22%, rgba(62, 207, 142, 0.08), transparent 30rem),
|
||||
radial-gradient(circle at 88% 70%, rgba(36, 180, 126, 0.04), transparent 24rem),
|
||||
linear-gradient(180deg, #090a0c 0%, #0c0e12 40%, #08090b 100%);
|
||||
color: var(--color-border-row);
|
||||
}
|
||||
.dashboard-header {
|
||||
position: sticky;
|
||||
z-index: 20;
|
||||
@@ -616,11 +628,6 @@
|
||||
.dashboard-main {
|
||||
width: min(100% - 32px, 1280px);
|
||||
}
|
||||
.status-header-inner,
|
||||
.status-main,
|
||||
.status-footer {
|
||||
width: min(100% - 32px, 960px);
|
||||
}
|
||||
.dashboard-main {
|
||||
padding: 40px 0 56px;
|
||||
}
|
||||
@@ -642,15 +649,6 @@
|
||||
.dashboard-header-inner {
|
||||
height: 62px;
|
||||
}
|
||||
.status-header-action {
|
||||
min-height: 36px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
.public-services-heading .icon-button {
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.dashboard-main {
|
||||
padding-top: 32px;
|
||||
}
|
||||
@@ -824,6 +822,11 @@
|
||||
--color-night-surface: #13161b;
|
||||
--color-night-card-alt: #15171b;
|
||||
--color-night-incident: #151114;
|
||||
--color-night-danger: #141113;
|
||||
--color-night-summary: #0d0e12;
|
||||
--color-night-banner-operational: #111815;
|
||||
--color-night-maintenance-icon: #161a22;
|
||||
--color-night-uptime-empty: #1c2028;
|
||||
--color-night-option-hover: #181c24;
|
||||
|
||||
/* Dark mode text tones */
|
||||
@@ -866,6 +869,11 @@
|
||||
--color-status-checking-bg: #c6aa38;
|
||||
--color-status-checking-dark: #e6c94b;
|
||||
|
||||
/* Uptime history bar colors */
|
||||
--color-uptime-empty: #e8ebe9;
|
||||
--color-uptime-down: #dc6262;
|
||||
--color-uptime-partial: #e1bc45;
|
||||
|
||||
/* Incident status & stream colors */
|
||||
--color-incident-border: #e8c3c3;
|
||||
--color-incident-bg: #fff8f8;
|
||||
@@ -874,6 +882,89 @@
|
||||
--color-incident-active-text: #aa4141;
|
||||
--color-incident-resolved-bg: #eaf9f2;
|
||||
|
||||
/* Status overall banner colors */
|
||||
--color-banner-operational-border: #b9e6d2;
|
||||
--color-banner-operational-text: #125c41;
|
||||
--color-banner-operational-from: #edfaf4;
|
||||
--color-banner-operational-to: #f8fdfb;
|
||||
|
||||
--color-banner-degraded-border: #e7d796;
|
||||
--color-banner-degraded-text: #705d0f;
|
||||
--color-banner-degraded-from: #fff9e8;
|
||||
--color-banner-degraded-to: #fffdf6;
|
||||
--color-banner-degraded-dark-text: #f6c86a;
|
||||
--color-banner-degraded-dark-border: rgba(224, 145, 63, 0.3);
|
||||
--color-banner-degraded-dark-from: rgba(224, 145, 63, 0.14);
|
||||
--color-banner-degraded-dark-via: rgba(180, 110, 24, 0.06);
|
||||
--color-banner-degraded-dark-to: rgba(28, 22, 14, 0.6);
|
||||
--color-banner-degraded-icon-border: rgb(178 145 24 / 0.25);
|
||||
--color-banner-degraded-dark-icon-border: rgba(224, 145, 63, 0.35);
|
||||
--color-banner-degraded-dark-icon-bg: rgba(28, 22, 14, 0.75);
|
||||
|
||||
--color-banner-down-border: #ebc1c1;
|
||||
--color-banner-down-text: #8e3030;
|
||||
--color-banner-down-to: #fffafa;
|
||||
--color-banner-down-dark-via: rgba(174, 61, 61, 0.06);
|
||||
--color-banner-down-dark-to: rgba(30, 16, 16, 0.6);
|
||||
--color-banner-down-icon-border: rgb(174 61 61 / 0.22);
|
||||
--color-banner-down-dark-icon-bg: rgba(30, 16, 16, 0.75);
|
||||
|
||||
/* Tone badge colors */
|
||||
--color-tone-critical-border: #e8caca;
|
||||
--color-tone-critical-bg: #fff4f4;
|
||||
--color-tone-critical-text: #a93e3e;
|
||||
--color-tone-major-border: #eddac2;
|
||||
--color-tone-major-bg: #fff8ef;
|
||||
--color-tone-major-text: #b25f18;
|
||||
--color-tone-minor-border: #e8dfc2;
|
||||
--color-tone-minor-bg: #fdfaee;
|
||||
--color-tone-minor-text: #8a6d1f;
|
||||
--color-tone-none-bg: #f5f5f5;
|
||||
|
||||
/* Scheduled maintenance theme tokens */
|
||||
--color-maintenance-card-border: #c9d9ed;
|
||||
--color-maintenance-header-border: #dce7f4;
|
||||
--color-maintenance-gradient-from: #f2f7fd;
|
||||
--color-maintenance-gradient-via: #f8fbff;
|
||||
--color-maintenance-icon: #3f6fb8;
|
||||
--color-maintenance-blue: #5a8fd0;
|
||||
--color-maintenance-blue-light: #72a4e8;
|
||||
--color-maintenance-overline: #5079b3;
|
||||
--color-maintenance-overline-dark: #7ba6e2;
|
||||
--color-maintenance-title: #29496f;
|
||||
--color-maintenance-title-dark: #e5edfa;
|
||||
--color-maintenance-badge-border: #cad9eb;
|
||||
--color-maintenance-badge-text: #466b9c;
|
||||
--color-maintenance-badge-text-dark: #8bb5ee;
|
||||
--color-maintenance-row-border: #e6edf6;
|
||||
--color-maintenance-icon-box-border: #d8e1ed;
|
||||
--color-maintenance-icon-box-bg: #f8fafd;
|
||||
|
||||
/* Active incident theme tokens */
|
||||
--color-active-incident-border: #e9c5c5;
|
||||
--color-active-incident-header-border: #f0dede;
|
||||
--color-active-incident-from: #fff4f4;
|
||||
--color-active-incident-via: #fffafa;
|
||||
--color-active-incident-overline: #a24a4a;
|
||||
--color-active-incident-title: #5d2929;
|
||||
--color-active-incident-badge-border: #e8caca;
|
||||
--color-active-incident-badge-text: #8e3f3f;
|
||||
--color-active-incident-row-border: #f1e5e5;
|
||||
--color-active-incident-icon-border: #e5d6d6;
|
||||
--color-active-incident-icon-bg: #fcf8f8;
|
||||
--color-active-incident-icon-text: #765454;
|
||||
--color-active-incident-subtext: #9a7777;
|
||||
--color-active-incident-body: #6f5a5a;
|
||||
--color-active-incident-meta: #8e4b4b;
|
||||
|
||||
/* Past incidents theme tokens */
|
||||
--color-past-incidents-header-border: #e8ecea;
|
||||
--color-past-incidents-from: #f4faf7;
|
||||
--color-past-incidents-via: #fbfdfc;
|
||||
--color-past-incidents-accent: #3f7a63;
|
||||
--color-past-incidents-title: #1f3b2f;
|
||||
--color-past-incidents-badge-border: #cfe3d9;
|
||||
|
||||
/* Borders & Dividers */
|
||||
--color-border-subtle: #dedede;
|
||||
--color-border-card: #e3e3e3;
|
||||
@@ -892,6 +983,12 @@
|
||||
--color-border-dialog-header: #e9ecea;
|
||||
--color-border-section: #edf0ee;
|
||||
--color-border-dialog-footer: #e4e8e6;
|
||||
--color-border-action: #d5d5d5;
|
||||
--color-border-accent-hover: #9bcdb7;
|
||||
--color-border-summary: #e9e9e9;
|
||||
--color-border-dashed-light: #dcdcdc;
|
||||
--color-border-skeleton: #e4e4e4;
|
||||
--color-border-footer: #e7ebe9;
|
||||
|
||||
/* Text & foreground tones */
|
||||
--color-ink-muted-dark: #888888;
|
||||
@@ -899,6 +996,14 @@
|
||||
--color-ink-faint-alt: #aaaaaa;
|
||||
--color-ink-code: #606060;
|
||||
--color-ink-body: #444444;
|
||||
--color-ink-caption: #a0a0a0;
|
||||
--color-ink-detail: #555555;
|
||||
--color-ink-label-muted: #4f4f4f;
|
||||
--color-ink-heading-dark: #292323;
|
||||
--color-ink-footer: #939393;
|
||||
--color-arrow-muted: #c4c4c4;
|
||||
--color-arrow-hover: #8f8f8f;
|
||||
--color-danger-detail: #ae3d3d;
|
||||
--color-accent-green: #17865b;
|
||||
--color-accent-green-hover: #16885b;
|
||||
--color-accent-green-deep: #11764e;
|
||||
@@ -948,14 +1053,28 @@
|
||||
--tracking-overline: 0.08em;
|
||||
--tracking-metric: -1px;
|
||||
--tracking-dialog-title: -0.35px;
|
||||
--tracking-hero: -2.5px;
|
||||
--tracking-hero-mobile: -1.8px;
|
||||
--tracking-incident-title: -0.65px;
|
||||
--tracking-metric-compact: -0.5px;
|
||||
--tracking-card-heading: -0.25px;
|
||||
--tracking-tight-sm: -0.2px;
|
||||
--tracking-tag: 0.04em;
|
||||
--tracking-mono-label: 0.06em;
|
||||
|
||||
/* Line height (leading) */
|
||||
--leading-hero: 1.02;
|
||||
--leading-title: 1.16;
|
||||
--leading-tight-md: 1.2;
|
||||
--leading-snug-sm: 1.3;
|
||||
--leading-entry: 1.35;
|
||||
--leading-overline: 1.4;
|
||||
--leading-alert: 1.45;
|
||||
--leading-subtitle: 1.55;
|
||||
--leading-body-relaxed: 1.6;
|
||||
|
||||
/* Typography scale (matching DESIGN.md and app UI) */
|
||||
--text-3xs: 9px;
|
||||
--text-2xs: 10px;
|
||||
--text-footnote: 11px;
|
||||
--text-micro: 12px;
|
||||
@@ -963,6 +1082,7 @@
|
||||
--text-body: 14px;
|
||||
--text-item: 15px;
|
||||
--text-body-md: 16px;
|
||||
--text-card-title: 17px;
|
||||
--text-subtitle: 19px;
|
||||
--text-brand-title: 20px;
|
||||
--text-dialog-title: 21px;
|
||||
@@ -971,6 +1091,8 @@
|
||||
--text-metric: 32px;
|
||||
--text-display-sm: 34px;
|
||||
--text-display: 40px;
|
||||
--text-incident-title: clamp(24px, 3vw, 30px);
|
||||
--text-hero-status: clamp(38px, 6vw, 56px);
|
||||
|
||||
/* Sizing and layout tokens */
|
||||
--spacing-panel-x: 26px;
|
||||
@@ -986,9 +1108,21 @@
|
||||
--spacing-select-max: 260px;
|
||||
--spacing-textarea-h: 154px;
|
||||
--spacing-textarea-sm-h: 132px;
|
||||
--spacing-timeline-col: 14px;
|
||||
--spacing-status-max: 960px;
|
||||
|
||||
/* Custom breakpoints */
|
||||
--breakpoint-compact: 440px;
|
||||
--breakpoint-mobile: 520px;
|
||||
--breakpoint-tablet: 760px;
|
||||
|
||||
/* Border width & Blur */
|
||||
--border-width-3: 3px;
|
||||
--blur-header: 14px;
|
||||
|
||||
/* Animation tokens */
|
||||
--animate-skeleton: skeleton 1.4s ease-in-out infinite;
|
||||
--animate-blink: blink 1.8s ease-in-out infinite;
|
||||
|
||||
/* Radius tokens */
|
||||
--radius-badge: 5px;
|
||||
@@ -999,8 +1133,30 @@
|
||||
|
||||
/* Shadows */
|
||||
--shadow-card-elevated: 0 18px 55px rgb(24 74 52 / 0.08), 0 2px 8px rgb(0 0 0 / 0.04);
|
||||
--shadow-card-elevated-green: 0 14px 40px rgb(28 65 49 / 0.04);
|
||||
--shadow-btn-primary: 0 1px 2px rgb(0 0 0 / 0.08), inset 0 1px rgb(255 255 255 / 0.2);
|
||||
--drop-shadow-brand-glow: 0 0 12px rgba(62, 207, 142, 0.45);
|
||||
--shadow-timeline-dot: 0 0 0 1px rgb(36 180 126 / 0.24);
|
||||
--shadow-maintenance-card: 0 14px 40px rgb(49 93 151 / 0.06);
|
||||
--shadow-active-incident: 0 14px 40px rgb(117 38 38 / 0.06);
|
||||
--shadow-active-incident-icon: 0 4px 14px rgb(130 34 34 / 0.06);
|
||||
--shadow-pulse-down: 0 0 0 4px rgb(217 92 92 / 0.1);
|
||||
--shadow-brand-dot: 0 0 0 3px rgb(62 207 142 / 0.12);
|
||||
--shadow-brand-dot-dark: 0 0 0 3px rgba(62, 207, 142, 0.2);
|
||||
--shadow-incident-detail: 0 12px 36px rgb(0 0 0 / 0.035);
|
||||
--shadow-incident-detail-dark: 0 16px 40px rgba(0, 0, 0, 0.35);
|
||||
--shadow-banner-operational: 0 12px 32px rgb(24 110 76 / 0.06);
|
||||
--shadow-banner-operational-dark: 0 16px 40px rgba(0, 0, 0, 0.4), 0 0 30px rgba(62, 207, 142, 0.05);
|
||||
--shadow-banner-degraded: 0 12px 32px rgb(130 105 14 / 0.06);
|
||||
--shadow-banner-degraded-dark: 0 16px 40px rgba(0, 0, 0, 0.4), 0 0 30px rgba(224, 145, 63, 0.06);
|
||||
--shadow-banner-down: 0 12px 32px rgb(130 34 34 / 0.06);
|
||||
--shadow-banner-down-dark: 0 16px 40px rgba(0, 0, 0, 0.4), 0 0 30px rgba(217, 92, 92, 0.06);
|
||||
|
||||
/* Background gradients */
|
||||
--background-image-skeleton-banner: linear-gradient(90deg, #eef2f0 20%, #f8faf9 50%, #eef2f0 80%);
|
||||
--background-image-skeleton-banner-dark: linear-gradient(90deg, #13161c 20%, #1a1e27 50%, #13161c 80%);
|
||||
--background-image-skeleton-item: linear-gradient(90deg, #f0f2f1 20%, #fafafa 50%, #f0f2f1 80%);
|
||||
--background-image-skeleton-item-dark: linear-gradient(90deg, #151821 20%, #1f2430 50%, #151821 80%);
|
||||
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user