mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 05:41:59 +00:00
fix: improve the moditor detail page
This commit is contained in:
@@ -26,7 +26,7 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
||||
if (data.length < 2)
|
||||
return (
|
||||
<Empty className="min-h-37.5 p-6">
|
||||
<EmptyTitle className="text-[13px]">Not enough latency data</EmptyTitle>
|
||||
<EmptyTitle className="text-caption">Not enough latency data</EmptyTitle>
|
||||
<EmptyDescription>More checks are needed to draw latency.</EmptyDescription>
|
||||
</Empty>
|
||||
);
|
||||
@@ -35,7 +35,11 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
||||
const maximum = Math.max(...values);
|
||||
|
||||
return (
|
||||
<div className="sparkline-wrap" role="img" aria-label={`Latency from ${minimum} to ${maximum} milliseconds`}>
|
||||
<div
|
||||
className="relative h-61.25 p-[26px_24px_24px] bg-linear-to-b from-surface-sparkline to-white dark:bg-[linear-gradient(180deg,rgb(62_207_142/0.025),transparent_55%),var(--color-night-chart)] dark:[&_.recharts-cartesian-grid_line]:stroke-white/7.5"
|
||||
role="img"
|
||||
aria-label={`Latency from ${minimum} to ${maximum} milliseconds`}
|
||||
>
|
||||
<ChartContainer config={latencyConfig} className="h-full w-full">
|
||||
<AreaChart data={data} margin={{ top: 10, right: 4, bottom: 4, left: 4 }}>
|
||||
<defs>
|
||||
@@ -50,7 +54,7 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="monitor-chart-tooltip min-w-36 gap-1.5 rounded-[6px] border-(--hairline) bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm"
|
||||
className="min-w-36 gap-1.5 rounded-sm border-hairline bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm dark:border-white/12 dark:bg-night-surface/96 dark:text-night-body dark:shadow-[0_16px_40px_rgb(0_0_0/0.55),inset_0_1px_rgb(255_255_255/0.045)]"
|
||||
labelFormatter={(_, payload) => new Date((payload[0].payload as LatencyDatum).t).toLocaleString()}
|
||||
/>
|
||||
}
|
||||
@@ -71,7 +75,7 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<div className="chart-scale">
|
||||
<div className="pointer-events-none absolute inset-[19px_22px_20px_auto] flex flex-col justify-between font-mono text-[9px]/none font-normal text-ink-faint-alt dark:text-night-scale">
|
||||
<span>{maximum} ms</span>
|
||||
<span>{minimum} ms</span>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ const formatUptimeTooltip: NonNullable<ComponentProps<typeof ChartTooltipContent
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-5">
|
||||
<span className="text-(--muted)">Successful</span>
|
||||
<span className="font-mono font-medium tabular-nums text-(--ink)">
|
||||
<span className="font-mono font-medium tabular-nums text-ink">
|
||||
{point.successfulChecks}/{point.totalChecks}
|
||||
</span>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
||||
if (data.length === 0)
|
||||
return (
|
||||
<Empty className="min-h-37.5 p-6">
|
||||
<EmptyTitle className="text-[13px]">No availability data yet</EmptyTitle>
|
||||
<EmptyTitle className="text-caption">No availability data yet</EmptyTitle>
|
||||
<EmptyDescription>No availability checks recorded yet.</EmptyDescription>
|
||||
</Empty>
|
||||
);
|
||||
@@ -91,7 +91,11 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
||||
const successfulChecks = checks.filter((check) => check.ok).length;
|
||||
return (
|
||||
<div>
|
||||
<div className="uptime-bar" role="img" aria-label={`${successfulChecks} of ${checks.length} recent checks succeeded`}>
|
||||
<div
|
||||
className="h-dialog-btn p-[24px_22px_18px] bg-linear-to-b from-surface-uptime to-white dark:bg-[linear-gradient(180deg,rgb(62_207_142/0.025),transparent_55%),var(--color-night-chart)]"
|
||||
role="img"
|
||||
aria-label={`${successfulChecks} of ${checks.length} recent checks succeeded`}
|
||||
>
|
||||
<ChartContainer config={uptimeConfig} className="h-full w-full">
|
||||
<BarChart data={data} barCategoryGap="24%" margin={{ top: 6, right: 0, bottom: 0, left: 0 }}>
|
||||
<XAxis dataKey="id" hide />
|
||||
@@ -99,7 +103,7 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="monitor-chart-tooltip min-w-36 gap-1.5 rounded-[6px] border-(--hairline) bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm"
|
||||
className="min-w-36 gap-1.5 rounded-sm border-hairline bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm dark:border-white/12 dark:bg-night-surface/96 dark:text-night-body dark:shadow-[0_16px_40px_rgb(0_0_0/0.55),inset_0_1px_rgb(255_255_255/0.045)]"
|
||||
hideIndicator={false}
|
||||
nameKey="status"
|
||||
labelFormatter={(_, payload) => formatBucketTime(payload[0].payload as UptimeDatum)}
|
||||
@@ -123,12 +127,13 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
<div className="uptime-legend">
|
||||
<span>Oldest</span>
|
||||
<span>
|
||||
<i className="legend-up" /> Up <i className="legend-degraded" /> Degraded <i className="legend-down" /> Down
|
||||
<div className="flex justify-between border-t border-border-divider p-[14px_22px_0] text-2xs text-ink-subtle-2 dark:border-white/6.5 dark:bg-night-elevated dark:text-night-faint-alt">
|
||||
<span className="inline-flex items-center gap-1.25">Oldest</span>
|
||||
<span className="inline-flex items-center gap-1.25">
|
||||
<i className="size-1.5 rounded-full bg-primary-deep" /> Up <i className="ml-1.25 size-1.5 rounded-full bg-chart-degraded" />{' '}
|
||||
Degraded <i className="ml-1.25 size-1.5 rounded-full bg-chart-down" /> Down
|
||||
</span>
|
||||
<span>Latest</span>
|
||||
<span className="inline-flex items-center gap-1.25">Latest</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Empty, EmptyContent, EmptyDescription, EmptyTitle } from '@/components/ui/empty';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AppHeader } from '../components/AppHeader';
|
||||
import { LatencySparkline } from '../components/charts/LatencySparkline';
|
||||
import { UptimeBar } from '../components/charts/UptimeBar';
|
||||
@@ -23,6 +24,18 @@ import {
|
||||
useRunCheckMutation,
|
||||
useUpdateMonitorMutation,
|
||||
} from '../queries/monitors';
|
||||
|
||||
const statusOrbVariants: Record<string, string> = {
|
||||
online:
|
||||
'border-status-online-border bg-primary-deep shadow-[0_0_0_8px_white] dark:border-brand/22 dark:bg-brand dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_24px_rgb(62_207_142/0.38)]',
|
||||
offline:
|
||||
'border-status-offline-border bg-status-offline-bg shadow-[0_0_0_8px_white] dark:border-red-400/24 dark:bg-red-400 dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_22px_rgb(248_113_113/0.32)]',
|
||||
pending:
|
||||
'border-status-pending-border bg-status-pending-bg shadow-[0_0_0_8px_white] dark:border-amber-500/24 dark:bg-status-pending-dark dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_22px_rgb(245_158_11/0.28)]',
|
||||
checking:
|
||||
'border-status-checking-border bg-status-checking-bg shadow-[0_0_0_8px_white] dark:border-yellow-400/22 dark:bg-status-checking-dark dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_22px_rgb(250_204_21/0.25)]',
|
||||
};
|
||||
|
||||
function formatInterval(seconds: number) {
|
||||
return INTERVAL_OPTIONS.find((option) => Number(option.value) === seconds)?.label ?? `${Math.round(seconds / 60)} min`;
|
||||
}
|
||||
@@ -76,29 +89,36 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
onCancel={() => setIsConfirmingDelete(false)}
|
||||
onConfirm={() => deleteMutation.mutate(monitor.id, { onSuccess: () => navigate('/dashboard') })}
|
||||
/>
|
||||
<main className="dashboard-main detail-main">
|
||||
<main className="dashboard-main pt-8.5 dark:relative dark:isolate dark:before:pointer-events-none dark:before:absolute dark:before:-top-27.5 dark:before:left-1/2 dark:before:-z-1 dark:before:h-107.5 dark:before:w-[min(920px,92vw)] dark:before:-translate-x-1/2 dark:before:content-[''] dark:before:[background:radial-gradient(circle_at_35%_20%,rgb(62_207_142/0.075),transparent_45%),linear-gradient(90deg,rgb(255_255_255/0.018)_1px,transparent_1px),linear-gradient(rgb(255_255_255/0.018)_1px,transparent_1px)] dark:before:bg-size-[auto,44px_44px,44px_44px] dark:before:mask-[linear-gradient(to_bottom,black_0%,transparent_82%)] max-[520px]:dark:before:-top-20 max-[520px]:dark:before:h-82.5 max-[520px]:dark:before:bg-size-[auto,34px_34px,34px_34px]">
|
||||
<Button variant="unstyled" className="back-link" type="button" onClick={() => navigate('/dashboard')}>
|
||||
<ArrowLeft /> All monitors
|
||||
</Button>
|
||||
<section className="detail-hero">
|
||||
<div className="detail-title">
|
||||
<span className={`status-orb ${status.variant}`} />
|
||||
<section className="mt-8.5 flex flex-col items-start justify-between gap-8 border-b border-border-hero pb-9 min-[761px]:flex-row min-[761px]:items-end dark:border-white/9 dark:animate-monitor-enter">
|
||||
<div className="flex min-w-0 items-center gap-4.5 max-[520px]:items-start">
|
||||
<span className={cn('size-4 shrink-0 rounded-full border-4 max-[520px]:mt-2.75', statusOrbVariants[status.variant])} />
|
||||
<div>
|
||||
<p className="overline">Monitor #{monitor.id}</p>
|
||||
<h1>{monitor.name}</h1>
|
||||
<a href={monitor.url} target="_blank" rel="noreferrer">
|
||||
<h1 className="m-0 truncate text-display-sm font-medium leading-[1.1] tracking-display text-foreground min-[761px]:text-display dark:text-slate-50 dark:[text-shadow:0_1px_18px_rgb(0_0_0/0.22)]">
|
||||
{monitor.name}
|
||||
</h1>
|
||||
<a
|
||||
href={monitor.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-2.25 inline-flex max-w-[min(70vw,680px)] items-center gap-1.5 truncate font-mono text-caption/overline font-normal text-(--muted) hover:text-primary-deep dark:text-night-muted-text dark:hover:text-brand-soft"
|
||||
>
|
||||
{monitor.url}
|
||||
<ExternalLink />
|
||||
<ExternalLink className="size-3.25 shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-actions">
|
||||
<div className="flex items-center gap-4.5 max-[760px]:flex-wrap">
|
||||
<Badge variant={status.variant} title={status.detail ?? undefined}>
|
||||
{status.label}
|
||||
</Badge>
|
||||
{!monitor.alertsEnabled && (
|
||||
<span className="muted-alert">
|
||||
<BellOff /> Alerts muted
|
||||
<span className="inline-flex items-center gap-1.5 text-micro text-(--muted) dark:text-night-muted-text">
|
||||
<BellOff className="size-3.5" /> Alerts muted
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
@@ -113,15 +133,20 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="sla-grid" aria-label="Uptime windows">
|
||||
<section
|
||||
className="mt-6 grid grid-cols-1 gap-3 min-[521px]:grid-cols-2 min-[761px]:grid-cols-3 min-[1001px]:grid-cols-5 dark:animate-monitor-enter dark:[animation-delay:45ms]"
|
||||
aria-label="Uptime windows"
|
||||
>
|
||||
{(['24h', '7d', '30d', '90d'] as const).map((key) => {
|
||||
const window = statsQuery.data?.windows[key];
|
||||
return (
|
||||
<Card asChild key={key}>
|
||||
<article className="sla-card">
|
||||
<p>{key} uptime</p>
|
||||
<strong>{window?.uptimePct == null ? '—' : `${window.uptimePct.toFixed(3)}%`}</strong>
|
||||
<span>
|
||||
<article className="min-w-0 rounded-md border border-border-card bg-white p-5 transition-[border-color,transform,box-shadow] duration-180 max-[520px]:p-4.5 dark:relative dark:border-white/8.5 dark:bg-[linear-gradient(145deg,var(--color-night-surface)_0%,var(--color-night-elevated)_100%)] dark:shadow-[0_10px_26px_rgb(0_0_0/0.22),inset_0_1px_rgb(255_255_255/0.035)] dark:before:absolute dark:before:-top-px dark:before:left-5 dark:before:h-px dark:before:w-8.5 dark:before:bg-brand/65 dark:before:content-[''] dark:hover:border-brand/24 dark:hover:-translate-y-0.5 dark:hover:shadow-[0_14px_34px_rgb(0_0_0/0.3),0_0_0_1px_rgb(62_207_142/0.035),inset_0_1px_rgb(255_255_255/0.05)]">
|
||||
<p className="m-0 mb-4.25 text-micro text-(--muted) dark:text-night-muted-text">{key} uptime</p>
|
||||
<strong className="block font-mono text-[23px]/none font-medium tracking-metric text-foreground dark:text-night-metric">
|
||||
{window?.uptimePct == null ? '—' : `${window.uptimePct.toFixed(3)}%`}
|
||||
</strong>
|
||||
<span className="mt-2.75 block truncate text-footnote text-faint dark:text-night-faint">
|
||||
{window?.totalChecks ?? 0} checks · {window?.avgLatencyMs ?? '—'} ms avg
|
||||
</span>
|
||||
</article>
|
||||
@@ -129,32 +154,51 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
);
|
||||
})}
|
||||
<Card asChild>
|
||||
<article className={`sla-card incident-summary ${openIncident ? 'has-incident' : ''}`}>
|
||||
<p>Current incident</p>
|
||||
<strong>{openIncident ? formatDuration(null, openIncident.startedAt) : 'None'}</strong>
|
||||
<span>{openIncident ? `Open since ${formatDate(openIncident.startedAt)}` : 'Everything is operational'}</span>
|
||||
<article
|
||||
className={cn(
|
||||
'min-w-0 rounded-md border border-border-card p-5 transition-[border-color,transform,box-shadow] duration-180 max-[520px]:p-4.5 max-[760px]:min-[521px]:col-span-full',
|
||||
"dark:relative dark:border-white/8.5 dark:shadow-[0_10px_26px_rgb(0_0_0/0.22),inset_0_1px_rgb(255_255_255/0.035)] dark:before:absolute dark:before:-top-px dark:before:left-5 dark:before:h-px dark:before:w-8.5 dark:before:content-[''] dark:hover:border-brand/24 dark:hover:-translate-y-0.5 dark:hover:shadow-[0_14px_34px_rgb(0_0_0/0.3),0_0_0_1px_rgb(62_207_142/0.035),inset_0_1px_rgb(255_255_255/0.05)]",
|
||||
openIncident
|
||||
? 'border-incident-border bg-incident-bg dark:border-red-400/30 dark:bg-[linear-gradient(145deg,rgb(73_28_31/0.58),var(--color-night-incident)_100%)] dark:before:bg-red-400'
|
||||
: 'bg-surface-soft dark:bg-[linear-gradient(145deg,var(--color-night-card-alt)_0%,var(--color-night-panel)_100%)] dark:before:bg-slate-400/48',
|
||||
)}
|
||||
>
|
||||
<p className="m-0 mb-4.25 text-micro text-(--muted) dark:text-night-muted-text">Current incident</p>
|
||||
<strong
|
||||
className={cn(
|
||||
'block font-mono text-[23px]/none font-medium tracking-metric text-foreground dark:text-night-metric',
|
||||
openIncident ? 'text-incident-text dark:text-red-300' : '',
|
||||
)}
|
||||
>
|
||||
{openIncident ? formatDuration(null, openIncident.startedAt) : 'None'}
|
||||
</strong>
|
||||
<span className="mt-2.75 block truncate text-footnote text-faint dark:text-night-faint">
|
||||
{openIncident ? `Open since ${formatDate(openIncident.startedAt)}` : 'Everything is operational'}
|
||||
</span>
|
||||
</article>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<div className="detail-grid">
|
||||
<section className="data-panel chart-panel">
|
||||
<div className="data-panel-heading">
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 min-[1001px]:grid-cols-[minmax(0,1.55fr)_minmax(300px,0.75fr)] dark:animate-monitor-enter dark:[animation-delay:90ms]">
|
||||
<section className="min-h-82.5 overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||
<div>
|
||||
<p className="overline">Response time</p>
|
||||
<h2>Latency</h2>
|
||||
<p className="overline mb-1.25">Response time</p>
|
||||
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Latency</h2>
|
||||
</div>
|
||||
<span>Last {checks.length} checks</span>
|
||||
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||
Last {checks.length} checks
|
||||
</span>
|
||||
</div>
|
||||
<LatencySparkline checks={checks} />
|
||||
</section>
|
||||
<section className="data-panel uptime-panel">
|
||||
<div className="data-panel-heading">
|
||||
<section className="min-h-82.5 overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||
<div>
|
||||
<p className="overline">Availability</p>
|
||||
<h2>Recent uptime</h2>
|
||||
<p className="overline mb-1.25">Availability</p>
|
||||
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Recent uptime</h2>
|
||||
</div>
|
||||
<span>
|
||||
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||
{checks.filter((check) => check.ok).length}/{checks.length} successful
|
||||
</span>
|
||||
</div>
|
||||
@@ -162,18 +206,20 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="detail-grid lower-grid">
|
||||
<section className="data-panel recent-checks-panel">
|
||||
<div className="data-panel-heading">
|
||||
<div className="mt-4 grid grid-cols-1 items-stretch gap-4 min-[1001px]:grid-cols-[minmax(0,1.55fr)_minmax(300px,0.75fr)] dark:animate-monitor-enter dark:[animation-delay:135ms]">
|
||||
<section className="flex h-full min-h-0 flex-col overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||
<div>
|
||||
<p className="overline">Event stream</p>
|
||||
<h2>Recent checks</h2>
|
||||
<p className="overline mb-1.25">Event stream</p>
|
||||
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Recent checks</h2>
|
||||
</div>
|
||||
<span>{Math.min(checks.length, 20)} shown</span>
|
||||
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||
{Math.min(checks.length, 20)} shown
|
||||
</span>
|
||||
</div>
|
||||
{/* Keyboard focus makes this horizontally scrollable region accessible without a pointer. */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
||||
<section className="data-table-wrap recent-checks-scroll" aria-label="Recent checks" tabIndex={0}>
|
||||
<section className="monitor-scroll overflow-x-auto" aria-label="Recent checks" tabIndex={0}>
|
||||
<Table className="min-w-150">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -185,12 +231,12 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{checks.slice(0, 20).map((check) => (
|
||||
<TableRow key={check.id}>
|
||||
<TableRow key={check.id} className="transition-colors duration-140 dark:hover:bg-white/[0.022]">
|
||||
<TableCell>
|
||||
<Badge variant={check.ok ? 'online' : 'offline'}>{check.ok ? 'Up' : 'Down'}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="max-w-65 truncate font-mono text-[11px]/[1.4] font-normal">
|
||||
<code className="max-w-select-max truncate font-mono text-footnote/overline font-normal dark:text-night-code">
|
||||
{check.statusCode ? `HTTP ${check.statusCode}` : (check.error ?? 'Failed')}
|
||||
</code>
|
||||
</TableCell>
|
||||
@@ -202,38 +248,62 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
</Table>
|
||||
{checks.length === 0 && (
|
||||
<Empty className="min-h-37.5 p-6">
|
||||
<EmptyTitle className="text-[13px]">No checks yet</EmptyTitle>
|
||||
<EmptyTitle className="text-caption">No checks yet</EmptyTitle>
|
||||
<EmptyDescription>No checks recorded.</EmptyDescription>
|
||||
</Empty>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
<section className="data-panel incidents-panel">
|
||||
<div className="data-panel-heading">
|
||||
<section className="flex h-full min-h-0 flex-col overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||
<div>
|
||||
<p className="overline">Downtime</p>
|
||||
<h2>Incidents</h2>
|
||||
<p className="overline mb-1.25">Downtime</p>
|
||||
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Incidents</h2>
|
||||
</div>
|
||||
<span>{incidents.length} recorded</span>
|
||||
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||
{incidents.length} recorded
|
||||
</span>
|
||||
</div>
|
||||
{/* Keyboard focus makes this scrollable region accessible without a pointer. */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
||||
<div className="incident-list" aria-label="Incident history" tabIndex={0}>
|
||||
<div className="monitor-scroll min-h-0" aria-label="Incident history" tabIndex={0}>
|
||||
{incidents.map((incident) => (
|
||||
<article className={incident.resolvedAt ? 'resolved' : 'open'} key={incident.id}>
|
||||
<span>{incident.resolvedAt ? <CheckCircle2 /> : <Clock3 />}</span>
|
||||
<div>
|
||||
<button type="button" className="incident-detail-link" onClick={() => navigate(`/incidents/${incident.id}`)}>
|
||||
<strong>{incident.title ?? (incident.resolvedAt ? 'Resolved incident' : 'Incident in progress')}</strong>
|
||||
<article
|
||||
key={incident.id}
|
||||
className="flex gap-3.25 p-[18px_20px] not-first:border-t not-first:border-border-row transition-colors duration-140 dark:not-first:border-white/6.5 dark:hover:bg-white/[0.022]"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'grid size-7.5 shrink-0 place-items-center rounded-full [&>svg]:size-3.75',
|
||||
incident.resolvedAt
|
||||
? 'bg-incident-resolved-bg text-accent-green-hover dark:bg-brand/10.5 dark:text-brand-soft dark:shadow-[inset_0_0_0_1px_rgb(62_207_142/0.12)]'
|
||||
: 'bg-incident-active-bg text-incident-active-text dark:bg-red-400/10 dark:text-red-400 dark:shadow-[inset_0_0_0_1px_rgb(248_113_113/0.14)]',
|
||||
)}
|
||||
>
|
||||
{incident.resolvedAt ? <CheckCircle2 /> : <Clock3 />}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<button
|
||||
type="button"
|
||||
className="group inline-flex cursor-pointer items-center gap-2.5 border-0 bg-transparent p-0 text-inherit"
|
||||
onClick={() => navigate(`/incidents/${incident.id}`)}
|
||||
>
|
||||
<strong className="text-caption font-medium text-foreground transition-colors group-hover:text-brand-deep dark:text-night-title dark:group-hover:text-brand-soft">
|
||||
{incident.title ?? (incident.resolvedAt ? 'Resolved incident' : 'Incident in progress')}
|
||||
</strong>
|
||||
<Badge variant={incident.resolvedAt ? 'online' : 'offline'}>{incident.status}</Badge>
|
||||
</button>
|
||||
<p>
|
||||
<p className="my-1.5 mb-1.5 mt-1 break-all text-micro leading-alert text-(--muted) dark:text-gray-400">
|
||||
{incident.latestUpdate?.body ??
|
||||
incident.startError ??
|
||||
(incident.startStatusCode ? `HTTP ${incident.startStatusCode}` : 'Endpoint became unavailable')}
|
||||
</p>
|
||||
{incident.latestUpdate && incident.startError && <small className="incident-raw">{incident.startError}</small>}
|
||||
<small>
|
||||
{incident.latestUpdate && incident.startError && (
|
||||
<small className="-mt-px mb-1.25 block break-all font-mono text-2xs/overline text-ink-faint-alt dark:text-night-faint-alt">
|
||||
{incident.startError}
|
||||
</small>
|
||||
)}
|
||||
<small className="font-mono text-2xs/overline text-ink-faint-alt dark:text-night-faint-alt">
|
||||
{formatDate(incident.startedAt)} · {formatDuration(incident.durationMs, incident.startedAt)}
|
||||
</small>
|
||||
</div>
|
||||
@@ -241,7 +311,7 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
))}
|
||||
{incidents.length === 0 && (
|
||||
<Empty className="min-h-37.5 p-6">
|
||||
<EmptyTitle className="text-[13px]">No incidents yet</EmptyTitle>
|
||||
<EmptyTitle className="text-caption">No incidents yet</EmptyTitle>
|
||||
<EmptyDescription>No downtime incidents recorded.</EmptyDescription>
|
||||
</Empty>
|
||||
)}
|
||||
@@ -249,56 +319,71 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="data-panel configuration-panel">
|
||||
<div className="data-panel-heading">
|
||||
<section className="mt-4 overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)] dark:animate-monitor-enter dark:[animation-delay:180ms]">
|
||||
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||
<div>
|
||||
<p className="overline">Setup</p>
|
||||
<h2>Configuration</h2>
|
||||
<p className="overline mb-1.25">Setup</p>
|
||||
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Configuration</h2>
|
||||
</div>
|
||||
<Button variant="unstyled" className="secondary-button" type="button" onClick={() => setIsEditing(true)}>
|
||||
<Pencil /> Edit
|
||||
</Button>
|
||||
</div>
|
||||
<dl className="config-list">
|
||||
<div className="config-row">
|
||||
<dt>Method</dt>
|
||||
<dd>{monitor.method}</dd>
|
||||
<dl className="m-0">
|
||||
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Method</dt>
|
||||
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">{monitor.method}</dd>
|
||||
</div>
|
||||
<div className="config-row">
|
||||
<dt>Expected status</dt>
|
||||
<dd>{monitor.expectedStatus}</dd>
|
||||
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Expected status</dt>
|
||||
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">{monitor.expectedStatus}</dd>
|
||||
</div>
|
||||
<div className="config-row">
|
||||
<dt>Check interval</dt>
|
||||
<dd>{formatInterval(monitor.intervalSeconds)}</dd>
|
||||
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Check interval</dt>
|
||||
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">
|
||||
{formatInterval(monitor.intervalSeconds)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="config-row">
|
||||
<dt>Timeout</dt>
|
||||
<dd>{monitor.timeoutMs.toLocaleString()} ms</dd>
|
||||
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Timeout</dt>
|
||||
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">
|
||||
{monitor.timeoutMs.toLocaleString()} ms
|
||||
</dd>
|
||||
</div>
|
||||
<div className="config-row">
|
||||
<dt>Scheduled checks</dt>
|
||||
<dd className="config-row-actions">
|
||||
<span>{monitor.enabled ? 'Running' : 'Paused'}</span>
|
||||
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Scheduled checks</dt>
|
||||
<dd className="m-0 flex items-center gap-3 font-sans">
|
||||
<Badge variant={monitor.enabled ? 'online' : 'pending'}>{monitor.enabled ? 'Running' : 'Paused'}</Badge>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="row-action"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
type="button"
|
||||
disabled={updateMutation.isPending}
|
||||
onClick={() => updateMutation.mutate({ id: monitor.id, input: { enabled: !monitor.enabled } })}
|
||||
className="h-7 gap-1.5 px-2.5 font-sans text-xs font-medium text-ink transition-colors hover:border-neutral-400 dark:border-white/12 dark:bg-white/4 dark:text-night-body dark:hover:border-white/25 dark:hover:bg-white/8"
|
||||
>
|
||||
{monitor.enabled ? <PowerOff /> : <Power />}
|
||||
{monitor.enabled ? 'Pause' : 'Resume'}
|
||||
{updateMutation.isPending ? (
|
||||
<RefreshCw className="size-3.5 animate-spin" />
|
||||
) : monitor.enabled ? (
|
||||
<PowerOff className="size-3.5" />
|
||||
) : (
|
||||
<Power className="size-3.5 text-brand-deep dark:text-brand-soft" />
|
||||
)}
|
||||
{updateMutation.isPending ? 'Updating…' : monitor.enabled ? 'Pause' : 'Resume'}
|
||||
</Button>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="config-row">
|
||||
<dt>Incident alerts</dt>
|
||||
<dd>{monitor.alertsEnabled ? 'Enabled' : 'Muted'}</dd>
|
||||
<div className="flex items-center justify-between gap-5 p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:hover:bg-white/[0.018]">
|
||||
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Incident alerts</dt>
|
||||
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">
|
||||
{monitor.alertsEnabled ? 'Enabled' : 'Muted'}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="config-danger">
|
||||
<p>This permanently deletes the monitor and its check history.</p>
|
||||
<div className="flex items-center justify-between gap-4 border-t border-border-row bg-surface-danger-subtle p-[16px_22px] max-[760px]:flex-col max-[760px]:items-start dark:border-t-red-400/14 dark:bg-night-muted dark:bg-[linear-gradient(90deg,rgb(127_29_29/0.11),transparent_52%)]">
|
||||
<p className="m-0 text-micro text-(--muted) dark:text-night-muted-text">
|
||||
This permanently deletes the monitor and its check history.
|
||||
</p>
|
||||
<Button variant="unstyled" className="danger-button" type="button" onClick={() => setIsConfirmingDelete(true)}>
|
||||
<Trash2 /> Delete monitor
|
||||
</Button>
|
||||
|
||||
+116
-1
@@ -3,7 +3,6 @@
|
||||
@import 'tw-animate-css';
|
||||
@import 'shadcn/tailwind.css';
|
||||
@import './styles/StatusPage.css';
|
||||
@import './styles/MonitorDetailPage.css';
|
||||
@import './styles/SettingsPage.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@@ -537,6 +536,16 @@
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
.dark .back-link {
|
||||
color: #858d9a;
|
||||
transition: color 160ms ease;
|
||||
}
|
||||
.dark .back-link:hover {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.dark .back-link svg {
|
||||
color: #4ade80;
|
||||
}
|
||||
.table-empty {
|
||||
display: grid;
|
||||
min-height: 150px;
|
||||
@@ -583,6 +592,16 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes monitor-detail-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.form-actions {
|
||||
@@ -661,10 +680,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
@utility monitor-scroll {
|
||||
max-height: clamp(320px, 52vh, 556px);
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-color: #b8c7c0 #f4f6f5;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid rgb(36 180 126 / 0.45);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f4f6f5;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border: 2px solid #f4f6f5;
|
||||
border-radius: 999px;
|
||||
background: #b8c7c0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: #91a69d;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .monitor-scroll {
|
||||
scrollbar-color: #3c4650 #0c0e11;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #0c0e11;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-color: #0c0e11;
|
||||
background: #3c4650;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: #53606b;
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'DM Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-mono: 'IBM Plex Mono', monospace;
|
||||
--animate-enter: enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
--animate-monitor-enter: monitor-detail-enter 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
|
||||
/* Brand emerald palette */
|
||||
--color-brand: #3ecf8e;
|
||||
@@ -672,6 +742,7 @@
|
||||
--color-brand-hover: #36c487;
|
||||
--color-brand-border: #35c586;
|
||||
--color-brand-light: #4ae09e;
|
||||
--color-brand-soft: #4ade80;
|
||||
--color-brand-muted: #55816e;
|
||||
--color-brand-dark-text: #090a0c;
|
||||
|
||||
@@ -681,6 +752,7 @@
|
||||
--color-faint: var(--faint);
|
||||
--color-soft: var(--soft);
|
||||
--color-night: var(--night);
|
||||
--color-primary-deep: var(--primary-deep);
|
||||
|
||||
/* Neutral text and icon shades */
|
||||
--color-ink-label: #353535;
|
||||
@@ -689,6 +761,7 @@
|
||||
--color-icon-muted: #5f5f5f;
|
||||
|
||||
/* Dark mode surfaces */
|
||||
--color-night-base: #090b0d;
|
||||
--color-night-root: #090a0c;
|
||||
--color-night-card: #111419;
|
||||
--color-night-input: #0b0d0f;
|
||||
@@ -699,6 +772,24 @@
|
||||
--color-night-item-hover: #1d222b;
|
||||
--color-night-item-active: #20252e;
|
||||
--color-night-dialog: #0e1116;
|
||||
--color-night-chart: #0d0f13;
|
||||
--color-night-elevated: #0f1115;
|
||||
--color-night-muted: #111215;
|
||||
--color-night-surface: #13161b;
|
||||
--color-night-card-alt: #15171b;
|
||||
--color-night-incident: #151114;
|
||||
|
||||
/* Dark mode text tones */
|
||||
--color-night-metric: #f4f7f6;
|
||||
--color-night-heading: #f3f5f7;
|
||||
--color-night-title: #e8ebef;
|
||||
--color-night-body: #e5e7eb;
|
||||
--color-night-code: #c7ced7;
|
||||
--color-night-muted-text: #929aa7;
|
||||
--color-night-subtle-text: #7e8794;
|
||||
--color-night-faint: #737c89;
|
||||
--color-night-faint-alt: #747d89;
|
||||
--color-night-scale: #68717e;
|
||||
|
||||
/* Light surfaces & backgrounds */
|
||||
--color-surface-soft: #fafafa;
|
||||
@@ -707,6 +798,28 @@
|
||||
--color-surface-hover: #fdfdfd;
|
||||
--color-surface-accent: #eef9f4;
|
||||
--color-surface-accent-hover: #e4f6ed;
|
||||
--color-surface-sparkline: #fdfefe;
|
||||
--color-surface-uptime: #fcfefd;
|
||||
--color-surface-danger-subtle: #fdfafa;
|
||||
|
||||
/* Status orb colors */
|
||||
--color-status-online-border: #c5f1df;
|
||||
--color-status-offline-border: #f2d4d4;
|
||||
--color-status-offline-bg: #c74f4f;
|
||||
--color-status-pending-border: #f6e2c8;
|
||||
--color-status-pending-bg: #d8892f;
|
||||
--color-status-pending-dark: #f0a440;
|
||||
--color-status-checking-border: #eee5bd;
|
||||
--color-status-checking-bg: #c6aa38;
|
||||
--color-status-checking-dark: #e6c94b;
|
||||
|
||||
/* Incident status & stream colors */
|
||||
--color-incident-border: #e8c3c3;
|
||||
--color-incident-bg: #fff8f8;
|
||||
--color-incident-text: #a83e3e;
|
||||
--color-incident-active-bg: #fff0f0;
|
||||
--color-incident-active-text: #aa4141;
|
||||
--color-incident-resolved-bg: #eaf9f2;
|
||||
|
||||
/* Borders & Dividers */
|
||||
--color-border-subtle: #dedede;
|
||||
@@ -717,6 +830,8 @@
|
||||
--color-border-input-hover: #aaaaaa;
|
||||
--color-border-accent: #c9eadb;
|
||||
--color-border-accent-checked: #8dd9b9;
|
||||
--color-border-hero: #e7e7e7;
|
||||
--color-border-divider: #f0f0f0;
|
||||
|
||||
/* Text & foreground tones */
|
||||
--color-ink-muted-dark: #888888;
|
||||
|
||||
@@ -1,879 +0,0 @@
|
||||
/* ==========================================================================
|
||||
Monitor Detail
|
||||
========================================================================== */
|
||||
|
||||
.detail-main {
|
||||
padding-top: 34px;
|
||||
}
|
||||
|
||||
.detail-hero {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
margin-top: 34px;
|
||||
padding-bottom: 36px;
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-orb {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 4px solid #f2d4d4;
|
||||
border-radius: 50%;
|
||||
background: #c74f4f;
|
||||
box-shadow: 0 0 0 8px #fff;
|
||||
}
|
||||
|
||||
.status-orb.online {
|
||||
border-color: #c5f1df;
|
||||
background: var(--primary-deep);
|
||||
}
|
||||
|
||||
.status-orb.pending {
|
||||
border-color: #f6e2c8;
|
||||
background: #d8892f;
|
||||
}
|
||||
|
||||
.status-orb.checking {
|
||||
border-color: #eee5bd;
|
||||
background: #c6aa38;
|
||||
}
|
||||
|
||||
.detail-title h1 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 40px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -1.5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-title a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: min(70vw, 680px);
|
||||
margin-top: 9px;
|
||||
overflow: hidden;
|
||||
font:
|
||||
400 13px/1.4 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: var(--muted);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-title a:hover {
|
||||
color: var(--primary-deep);
|
||||
}
|
||||
|
||||
.detail-title a svg {
|
||||
flex: 0 0 auto;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.muted-alert {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.muted-alert svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.sla-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.sla-card {
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sla-card p {
|
||||
margin: 0 0 17px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.sla-card strong {
|
||||
display: block;
|
||||
font:
|
||||
500 23px/1 'IBM Plex Mono',
|
||||
monospace;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.sla-card span {
|
||||
display: block;
|
||||
margin-top: 11px;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
color: var(--faint);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sla-card.incident-summary {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.sla-card.incident-summary.has-incident {
|
||||
border-color: #e8c3c3;
|
||||
background: #fff8f8;
|
||||
}
|
||||
|
||||
.sla-card.incident-summary.has-incident strong {
|
||||
color: #a83e3e;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(300px, 0.75fr);
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.detail-grid.lower-grid {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.lower-grid > .data-panel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.data-panel {
|
||||
overflow: hidden;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.data-panel-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
min-height: 82px;
|
||||
padding: 18px 22px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.data-panel-heading .overline {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.data-panel-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.data-panel-heading > span {
|
||||
font:
|
||||
400 11px/1.4 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
.configuration-panel {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.config-list {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 14px 22px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.config-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.config-row dt {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.config-row dd {
|
||||
margin: 0;
|
||||
font:
|
||||
400 13px/1.4 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.config-row .config-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.config-row-actions .row-action {
|
||||
font-family: 'DM Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.config-danger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 22px;
|
||||
border-top: 1px solid #ededed;
|
||||
background: #fdfafa;
|
||||
}
|
||||
|
||||
.config-danger p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chart-panel,
|
||||
.uptime-panel {
|
||||
min-height: 330px;
|
||||
}
|
||||
|
||||
.sparkline-wrap {
|
||||
position: relative;
|
||||
height: 245px;
|
||||
padding: 26px 24px 24px;
|
||||
background: linear-gradient(180deg, #fdfefe, #fff);
|
||||
}
|
||||
|
||||
.chart-scale {
|
||||
position: absolute;
|
||||
inset: 19px 22px 20px auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
font:
|
||||
400 9px/1 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.uptime-bar {
|
||||
height: 148px;
|
||||
padding: 24px 22px 18px;
|
||||
background: linear-gradient(180deg, #fcfefd 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.uptime-legend {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 14px 22px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.uptime-legend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.uptime-legend i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-deep);
|
||||
}
|
||||
|
||||
.uptime-legend i.legend-down {
|
||||
margin-left: 5px;
|
||||
background: var(--chart-down);
|
||||
}
|
||||
|
||||
.uptime-legend i.legend-degraded {
|
||||
margin-left: 5px;
|
||||
background: var(--chart-degraded);
|
||||
}
|
||||
|
||||
.data-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.recent-checks-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recent-checks-scroll,
|
||||
.incident-list {
|
||||
max-height: clamp(320px, 52vh, 556px);
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-color: #b8c7c0 #f4f6f5;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.recent-checks-scroll:focus-visible,
|
||||
.incident-list:focus-visible {
|
||||
outline: 2px solid rgb(36 180 126 / 0.45);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.recent-checks-scroll::-webkit-scrollbar,
|
||||
.incident-list::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.recent-checks-scroll::-webkit-scrollbar-track,
|
||||
.incident-list::-webkit-scrollbar-track {
|
||||
background: #f4f6f5;
|
||||
}
|
||||
|
||||
.recent-checks-scroll::-webkit-scrollbar-thumb,
|
||||
.incident-list::-webkit-scrollbar-thumb {
|
||||
border: 2px solid #f4f6f5;
|
||||
border-radius: 999px;
|
||||
background: #b8c7c0;
|
||||
}
|
||||
|
||||
.recent-checks-scroll::-webkit-scrollbar-thumb:hover,
|
||||
.incident-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #91a69d;
|
||||
}
|
||||
|
||||
.incident-list {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.incident-list article {
|
||||
display: flex;
|
||||
gap: 13px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.incident-list article > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.incident-list article + article {
|
||||
border-top: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.incident-list article > span {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
color: #16885b;
|
||||
background: #eaf9f2;
|
||||
}
|
||||
|
||||
.incident-list article.open > span {
|
||||
color: #aa4141;
|
||||
background: #fff0f0;
|
||||
}
|
||||
|
||||
.incident-list article svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.incident-list strong {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.incident-list p {
|
||||
margin: 4px 0 6px;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.incident-list small {
|
||||
font:
|
||||
400 10px/1.4 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.incident-list .incident-raw {
|
||||
display: block;
|
||||
margin: -1px 0 5px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.incident-detail-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.sla-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.detail-grid,
|
||||
.detail-grid.lower-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.detail-hero {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.config-danger {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-title h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.sla-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.sla-card.incident-summary {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.detail-title {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.status-orb {
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.sla-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sla-card.incident-summary {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.data-panel-heading {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark operations console. */
|
||||
|
||||
.dark .detail-main {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.dark .detail-main::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: -110px;
|
||||
left: 50%;
|
||||
width: min(920px, 92vw);
|
||||
height: 430px;
|
||||
background:
|
||||
radial-gradient(circle at 35% 20%, rgb(62 207 142 / 0.075), transparent 45%),
|
||||
linear-gradient(90deg, rgb(255 255 255 / 0.018) 1px, transparent 1px), linear-gradient(rgb(255 255 255 / 0.018) 1px, transparent 1px);
|
||||
background-size:
|
||||
auto,
|
||||
44px 44px,
|
||||
44px 44px;
|
||||
content: '';
|
||||
mask-image: linear-gradient(to bottom, #000 0%, transparent 82%);
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.dark .back-link {
|
||||
color: #858d9a;
|
||||
transition: color 160ms ease;
|
||||
}
|
||||
|
||||
.dark .back-link:hover {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.dark .back-link svg {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.dark .detail-hero {
|
||||
border-bottom-color: rgb(255 255 255 / 0.09);
|
||||
}
|
||||
|
||||
.dark .detail-title h1 {
|
||||
color: #f8fafc;
|
||||
text-shadow: 0 1px 18px rgb(0 0 0 / 0.22);
|
||||
}
|
||||
|
||||
.dark .detail-title a {
|
||||
color: #929aa7;
|
||||
}
|
||||
|
||||
.dark .detail-title a:hover {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.dark .status-orb {
|
||||
border-color: rgb(248 113 113 / 0.24);
|
||||
background: #f87171;
|
||||
box-shadow:
|
||||
0 0 0 8px #090b0d,
|
||||
0 0 22px rgb(248 113 113 / 0.32);
|
||||
}
|
||||
|
||||
.dark .status-orb.online {
|
||||
border-color: rgb(62 207 142 / 0.22);
|
||||
background: #3ecf8e;
|
||||
box-shadow:
|
||||
0 0 0 8px #090b0d,
|
||||
0 0 24px rgb(62 207 142 / 0.38);
|
||||
}
|
||||
|
||||
.dark .status-orb.pending {
|
||||
border-color: rgb(245 158 11 / 0.24);
|
||||
background: #f0a440;
|
||||
box-shadow:
|
||||
0 0 0 8px #090b0d,
|
||||
0 0 22px rgb(245 158 11 / 0.28);
|
||||
}
|
||||
|
||||
.dark .status-orb.checking {
|
||||
border-color: rgb(250 204 21 / 0.22);
|
||||
background: #e6c94b;
|
||||
box-shadow:
|
||||
0 0 0 8px #090b0d,
|
||||
0 0 22px rgb(250 204 21 / 0.25);
|
||||
}
|
||||
|
||||
.dark .muted-alert {
|
||||
color: #929aa7;
|
||||
}
|
||||
|
||||
.dark .sla-card {
|
||||
position: relative;
|
||||
border-color: rgb(255 255 255 / 0.085);
|
||||
background: linear-gradient(145deg, #13161b 0%, #0f1115 100%);
|
||||
box-shadow:
|
||||
0 10px 26px rgb(0 0 0 / 0.22),
|
||||
inset 0 1px rgb(255 255 255 / 0.035);
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
transform 180ms ease,
|
||||
box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.dark .sla-card::before {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 20px;
|
||||
width: 34px;
|
||||
height: 1px;
|
||||
background: rgb(62 207 142 / 0.65);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.dark .sla-card p {
|
||||
color: #929aa7;
|
||||
}
|
||||
|
||||
.dark .sla-card strong {
|
||||
color: #f4f7f6;
|
||||
}
|
||||
|
||||
.dark .sla-card span {
|
||||
color: #737c89;
|
||||
}
|
||||
|
||||
.dark .sla-card.incident-summary {
|
||||
background: linear-gradient(145deg, #15171b 0%, #101216 100%);
|
||||
}
|
||||
|
||||
.dark .sla-card.incident-summary::before {
|
||||
background: rgb(148 163 184 / 0.48);
|
||||
}
|
||||
|
||||
.dark .sla-card.incident-summary.has-incident {
|
||||
border-color: rgb(248 113 113 / 0.3);
|
||||
background: linear-gradient(145deg, rgb(73 28 31 / 0.58), #151114 100%);
|
||||
}
|
||||
|
||||
.dark .sla-card.incident-summary.has-incident::before {
|
||||
background: #f87171;
|
||||
}
|
||||
|
||||
.dark .sla-card.incident-summary.has-incident strong {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.dark .sla-card:hover {
|
||||
border-color: rgb(62 207 142 / 0.24);
|
||||
box-shadow:
|
||||
0 14px 34px rgb(0 0 0 / 0.3),
|
||||
0 0 0 1px rgb(62 207 142 / 0.035),
|
||||
inset 0 1px rgb(255 255 255 / 0.05);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.dark .data-panel {
|
||||
border-color: rgb(255 255 255 / 0.085);
|
||||
background: #101216;
|
||||
box-shadow:
|
||||
0 16px 38px rgb(0 0 0 / 0.27),
|
||||
inset 0 1px rgb(255 255 255 / 0.03);
|
||||
}
|
||||
|
||||
.dark .data-panel-heading {
|
||||
border-bottom-color: rgb(255 255 255 / 0.075);
|
||||
background: linear-gradient(180deg, rgb(255 255 255 / 0.022), transparent);
|
||||
}
|
||||
|
||||
.dark .data-panel-heading h2 {
|
||||
color: #f3f5f7;
|
||||
}
|
||||
|
||||
.dark .data-panel-heading > span {
|
||||
color: #7e8794;
|
||||
}
|
||||
|
||||
.dark .sparkline-wrap,
|
||||
.dark .uptime-bar {
|
||||
background: linear-gradient(180deg, rgb(62 207 142 / 0.025), transparent 55%), #0d0f13;
|
||||
}
|
||||
|
||||
.dark .sparkline-wrap .recharts-cartesian-grid line {
|
||||
stroke: rgb(255 255 255 / 0.075);
|
||||
}
|
||||
|
||||
.dark .chart-scale {
|
||||
color: #68717e;
|
||||
}
|
||||
|
||||
.dark .monitor-chart-tooltip {
|
||||
border-color: rgb(255 255 255 / 0.12);
|
||||
color: #e5e7eb;
|
||||
background: rgb(19 22 27 / 0.96);
|
||||
box-shadow:
|
||||
0 16px 40px rgb(0 0 0 / 0.55),
|
||||
inset 0 1px rgb(255 255 255 / 0.045);
|
||||
}
|
||||
|
||||
.dark .uptime-legend {
|
||||
border-top-color: rgb(255 255 255 / 0.065);
|
||||
color: #747d89;
|
||||
background: #0f1115;
|
||||
}
|
||||
|
||||
.dark .recent-checks-scroll,
|
||||
.dark .incident-list {
|
||||
scrollbar-color: #3c4650 #0c0e11;
|
||||
}
|
||||
|
||||
.dark .recent-checks-scroll::-webkit-scrollbar-track,
|
||||
.dark .incident-list::-webkit-scrollbar-track {
|
||||
background: #0c0e11;
|
||||
}
|
||||
|
||||
.dark .recent-checks-scroll::-webkit-scrollbar-thumb,
|
||||
.dark .incident-list::-webkit-scrollbar-thumb {
|
||||
border-color: #0c0e11;
|
||||
background: #3c4650;
|
||||
}
|
||||
|
||||
.dark .recent-checks-scroll::-webkit-scrollbar-thumb:hover,
|
||||
.dark .incident-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #53606b;
|
||||
}
|
||||
|
||||
.dark .recent-checks-panel [data-slot='table-row'] {
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
|
||||
.dark .recent-checks-panel [data-slot='table-body'] [data-slot='table-row']:hover {
|
||||
background: rgb(255 255 255 / 0.022);
|
||||
}
|
||||
|
||||
.dark .recent-checks-panel code {
|
||||
color: #c7ced7;
|
||||
}
|
||||
|
||||
.dark .incident-list article + article {
|
||||
border-top-color: rgb(255 255 255 / 0.065);
|
||||
}
|
||||
|
||||
.dark .incident-list article {
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
|
||||
.dark .incident-list article:hover {
|
||||
background: rgb(255 255 255 / 0.022);
|
||||
}
|
||||
|
||||
.dark .incident-list article > span {
|
||||
color: #4ade80;
|
||||
background: rgb(62 207 142 / 0.105);
|
||||
box-shadow: inset 0 0 0 1px rgb(62 207 142 / 0.12);
|
||||
}
|
||||
|
||||
.dark .incident-list article.open > span {
|
||||
color: #f87171;
|
||||
background: rgb(248 113 113 / 0.1);
|
||||
box-shadow: inset 0 0 0 1px rgb(248 113 113 / 0.14);
|
||||
}
|
||||
|
||||
.dark .incident-list strong {
|
||||
color: #e8ebef;
|
||||
}
|
||||
|
||||
.dark .incident-detail-link:hover strong {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.dark .incident-list p {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark .incident-list small {
|
||||
color: #747d89;
|
||||
}
|
||||
|
||||
.dark .config-row {
|
||||
border-bottom-color: rgb(255 255 255 / 0.065);
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
|
||||
.dark .config-row:hover {
|
||||
background: rgb(255 255 255 / 0.018);
|
||||
}
|
||||
|
||||
.dark .config-row dt {
|
||||
color: #929aa7;
|
||||
}
|
||||
|
||||
.dark .config-row dd {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .config-row-actions > span {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.dark .config-danger {
|
||||
border-top-color: rgb(248 113 113 / 0.14);
|
||||
background: linear-gradient(90deg, rgb(127 29 29 / 0.11), transparent 52%), #111215;
|
||||
}
|
||||
|
||||
.dark .config-danger p {
|
||||
color: #929aa7;
|
||||
}
|
||||
|
||||
.dark .detail-hero,
|
||||
.dark .sla-grid,
|
||||
.dark .detail-grid,
|
||||
.dark .configuration-panel {
|
||||
animation: monitor-detail-enter 420ms both cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.dark .sla-grid {
|
||||
animation-delay: 45ms;
|
||||
}
|
||||
|
||||
.dark .detail-grid {
|
||||
animation-delay: 90ms;
|
||||
}
|
||||
|
||||
.dark .detail-grid.lower-grid {
|
||||
animation-delay: 135ms;
|
||||
}
|
||||
|
||||
.dark .configuration-panel {
|
||||
animation-delay: 180ms;
|
||||
}
|
||||
|
||||
@keyframes monitor-detail-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.dark .detail-main::before {
|
||||
top: -80px;
|
||||
height: 330px;
|
||||
background-size:
|
||||
auto,
|
||||
34px 34px,
|
||||
34px 34px;
|
||||
}
|
||||
|
||||
.dark .sla-card {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user