fix: improve the dashboard ui

This commit is contained in:
2026-09-03 21:23:41 +07:00
parent 604e83666d
commit a13fd064d8
14 changed files with 602 additions and 450 deletions
+2 -2
View File
@@ -36,8 +36,8 @@ const formatUptimeTooltip: NonNullable<ComponentProps<typeof ChartTooltipContent
const point = item.payload as UptimeDatum;
return (
<div className="flex w-full items-center justify-between gap-5">
<span className="text-[#777]">Successful</span>
<span className="font-mono font-medium tabular-nums text-[#171717]">
<span className="text-(--muted)">Successful</span>
<span className="font-mono font-medium tabular-nums text-(--ink)">
{point.successfulChecks}/{point.totalChecks}
</span>
</div>
@@ -31,14 +31,14 @@ export function AiComposeField({
<div className="ai-compose-pane">
<div className="ai-compose-pane-header">
<label htmlFor="incident-note">Internal note</label>
<small>Admin only</small>
<small>Admin only · {note.length}/1000</small>
</div>
<textarea
id="incident-note"
value={note}
onChange={(event) => onNoteChange(event.target.value)}
maxLength={1000}
placeholder="redis full memory, scaling capacity"
placeholder="e.g. Redis is at capacity; scaling is in progress"
/>
{enabled ? (
<Button
@@ -64,12 +64,13 @@ export function AiComposeField({
<div className="ai-compose-pane">
<div className="ai-compose-pane-header">
<label htmlFor="incident-public-body">Public update</label>
<small>Customer-facing</small>
<small>Customer-facing · {body.length}/2000</small>
</div>
<textarea
id="incident-public-body"
value={body}
onChange={(event) => onBodyChange(event.target.value)}
placeholder="Explain the impact and what your team is doing"
maxLength={2000}
required
aria-busy={isPending}
@@ -29,6 +29,7 @@ export function IncidentDialog({ onClose }: { onClose(): void }) {
monitorIds: [],
});
const [generated, setGenerated] = useState(false);
const availableMonitors = monitors.data?.monitors ?? [];
function generate() {
draft.mutate(
{ note: form.note, status: form.status, monitorIds: form.monitorIds },
@@ -46,94 +47,106 @@ export function IncidentDialog({ onClose }: { onClose(): void }) {
}
return (
<Dialog open onOpenChange={(open) => !open && !create.isPending && onClose()}>
<DialogContent className="incident-dialog max-h-[90vh] w-[calc(100%-2rem)] overflow-y-auto sm:max-w-2xl">
<DialogContent className="incident-dialog">
<DialogHeader>
<p className="overline">Incident management</p>
<DialogTitle>Declare incident</DialogTitle>
<DialogDescription>Turn a short internal note into a clear customer-facing update.</DialogDescription>
</DialogHeader>
<form className="incident-form" onSubmit={submit}>
<div className="incident-form-grid">
<div className="field">
<span id="incident-status-label">Status</span>
<Select value={form.status} onValueChange={(status) => setForm({ ...form, status: status as IncidentStatus })}>
<SelectTrigger aria-labelledby="incident-status-label">
<SelectValue>
<IncidentStatusOption value={form.status} />
</SelectValue>
</SelectTrigger>
<SelectContent>
{INCIDENT_STATUSES.map((status) => (
<SelectItem key={status} value={status}>
<IncidentStatusOption value={status} />
</SelectItem>
))}
</SelectContent>
</Select>
<div className="incident-form-body">
<div className="incident-form-grid">
<div className="field">
<span id="incident-status-label">Status</span>
<Select value={form.status} onValueChange={(status) => setForm({ ...form, status: status as IncidentStatus })}>
<SelectTrigger aria-labelledby="incident-status-label">
<SelectValue>
<IncidentStatusOption value={form.status} />
</SelectValue>
</SelectTrigger>
<SelectContent>
{INCIDENT_STATUSES.map((status) => (
<SelectItem key={status} value={status}>
<IncidentStatusOption value={status} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="field">
<span id="incident-impact-label">Impact</span>
<Select value={form.impact} onValueChange={(impact) => setForm({ ...form, impact: impact as IncidentImpact })}>
<SelectTrigger aria-labelledby="incident-impact-label">
<SelectValue>
<IncidentImpactOption value={form.impact} />
</SelectValue>
</SelectTrigger>
<SelectContent>
{INCIDENT_IMPACTS.map((impact) => (
<SelectItem key={impact} value={impact}>
<IncidentImpactOption value={impact} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<label className="field incident-title-field" htmlFor="incident-title">
<span>Public title</span>
<Input
id="incident-title"
value={form.title}
onChange={(event) => setForm({ ...form, title: event.target.value })}
placeholder="Briefly describe the disruption"
maxLength={120}
required
/>
</label>
</div>
<div className="field">
<span id="incident-impact-label">Impact</span>
<Select value={form.impact} onValueChange={(impact) => setForm({ ...form, impact: impact as IncidentImpact })}>
<SelectTrigger aria-labelledby="incident-impact-label">
<SelectValue>
<IncidentImpactOption value={form.impact} />
</SelectValue>
</SelectTrigger>
<SelectContent>
{INCIDENT_IMPACTS.map((impact) => (
<SelectItem key={impact} value={impact}>
<IncidentImpactOption value={impact} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<label className="field incident-title-field" htmlFor="incident-title">
<span>Public title</span>
<Input
id="incident-title"
value={form.title}
onChange={(event) => setForm({ ...form, title: event.target.value })}
maxLength={120}
required
/>
</label>
<fieldset className="incident-service-picker">
<legend>
Affected services <small>Optional</small>
</legend>
<div className="incident-service-options">
{availableMonitors.length ? (
availableMonitors.map((monitor) => (
<label key={monitor.id}>
<input
type="checkbox"
checked={form.monitorIds.includes(monitor.id)}
onChange={(event) =>
setForm({
...form,
monitorIds: event.target.checked
? [...form.monitorIds, monitor.id]
: form.monitorIds.filter((id) => id !== monitor.id),
})
}
/>
<span>{monitor.name}</span>
</label>
))
) : (
<p className="incident-service-empty">{monitors.isPending ? 'Loading services…' : 'No monitors available'}</p>
)}
</div>
</fieldset>
<AiComposeField
note={form.note}
body={form.body}
onNoteChange={(note) => setForm({ ...form, note })}
onBodyChange={(body) => setForm({ ...form, body })}
onGenerate={generate}
isPending={draft.isPending}
error={draft.error}
generated={generated}
/>
{create.isError && (
<p className="form-error" role="alert">
{create.error.message}
</p>
)}
</div>
<fieldset className="incident-service-picker">
<legend>
Affected services <small>Optional</small>
</legend>
<div className="incident-service-options">
{monitors.data?.monitors.map((monitor) => (
<label key={monitor.id}>
<input
type="checkbox"
checked={form.monitorIds.includes(monitor.id)}
onChange={(event) =>
setForm({
...form,
monitorIds: event.target.checked
? [...form.monitorIds, monitor.id]
: form.monitorIds.filter((id) => id !== monitor.id),
})
}
/>
<span>{monitor.name}</span>
</label>
))}
</div>
</fieldset>
<AiComposeField
note={form.note}
body={form.body}
onNoteChange={(note) => setForm({ ...form, note })}
onBodyChange={(body) => setForm({ ...form, body })}
onGenerate={generate}
isPending={draft.isPending}
error={draft.error}
generated={generated}
/>
<div className="form-actions compact-actions">
<div className="incident-dialog-actions">
<Button variant="unstyled" className="secondary-button" type="button" onClick={onClose}>
Cancel
</Button>
@@ -141,11 +154,6 @@ export function IncidentDialog({ onClose }: { onClose(): void }) {
{create.isPending ? 'Publishing…' : 'Declare incident'}
</Button>
</div>
{create.isError && (
<p className="form-error" role="alert">
{create.error.message}
</p>
)}
</form>
</DialogContent>
</Dialog>
@@ -20,51 +20,58 @@ export function IncidentUpdateDialog({ incident, onClose }: { incident: Incident
}
return (
<Dialog open onOpenChange={(open) => !open && !post.isPending && onClose()}>
<DialogContent className="incident-dialog max-h-[90vh] w-[calc(100%-2rem)] overflow-y-auto sm:max-w-2xl">
<DialogContent className="incident-dialog">
<DialogHeader>
<p className="overline">Incident update</p>
<DialogTitle>{incident.title ?? 'Service disruption'}</DialogTitle>
<DialogDescription>Publish the next update and advance the incident lifecycle.</DialogDescription>
</DialogHeader>
<form className="incident-form" onSubmit={submit}>
<div className="field">
<span id="update-status-label">Status</span>
<Select value={status} onValueChange={(value) => setStatus(value as IncidentStatus)}>
<SelectTrigger aria-labelledby="update-status-label">
<SelectValue>
<IncidentStatusOption value={status} />
</SelectValue>
</SelectTrigger>
<SelectContent>
{INCIDENT_STATUSES.map((value) => (
<SelectItem key={value} value={value}>
<IncidentStatusOption value={value} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<AiComposeField
note={note}
body={body}
onNoteChange={setNote}
onBodyChange={setBody}
onGenerate={() =>
draft.mutate(
{ note, status },
{
onSuccess: (result) => {
setBody(result.body);
setGenerated(true);
<div className="incident-form-body">
<div className="field incident-update-status">
<span id="update-status-label">Status</span>
<Select value={status} onValueChange={(value) => setStatus(value as IncidentStatus)}>
<SelectTrigger aria-labelledby="update-status-label">
<SelectValue>
<IncidentStatusOption value={status} />
</SelectValue>
</SelectTrigger>
<SelectContent>
{INCIDENT_STATUSES.map((value) => (
<SelectItem key={value} value={value}>
<IncidentStatusOption value={value} />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<AiComposeField
note={note}
body={body}
onNoteChange={setNote}
onBodyChange={setBody}
onGenerate={() =>
draft.mutate(
{ note, status },
{
onSuccess: (result) => {
setBody(result.body);
setGenerated(true);
},
},
},
)
}
isPending={draft.isPending}
error={draft.error}
generated={generated}
/>
<div className="form-actions compact-actions">
)
}
isPending={draft.isPending}
error={draft.error}
generated={generated}
/>
{post.isError && (
<p className="form-error" role="alert">
{post.error.message}
</p>
)}
</div>
<div className="incident-dialog-actions">
<Button variant="unstyled" className="secondary-button" type="button" onClick={onClose}>
Cancel
</Button>
@@ -72,11 +79,6 @@ export function IncidentUpdateDialog({ incident, onClose }: { incident: Incident
{post.isPending ? 'Publishing…' : status === 'resolved' ? 'Resolve incident' : 'Post update'}
</Button>
</div>
{post.isError && (
<p className="form-error" role="alert">
{post.error.message}
</p>
)}
</form>
</DialogContent>
</Dialog>
+8 -1
View File
@@ -1815,6 +1815,13 @@ button {
}
.dark {
/* Semantic text and surface tokens used by both page styles and shared UI. */
--ink: #f3f4f6;
--muted: #aeb4bf;
--faint: #858d9a;
--hairline: rgb(255 255 255 / 0.12);
--soft: #171a20;
--primary-deep: #3ecf8e;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
@@ -1826,7 +1833,7 @@ button {
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--shadcn-muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--muted-foreground: oklch(0.76 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
+149 -32
View File
@@ -498,35 +498,63 @@
text-align: left;
cursor: pointer;
}
.incident-dialog {
display: flex;
width: min(860px, calc(100% - 32px));
max-width: 860px;
max-height: min(90dvh, 820px);
flex-direction: column;
padding: 0;
gap: 0;
overflow: hidden;
border-radius: 14px;
}
.incident-dialog [data-slot='dialog-header'] {
gap: 5px;
padding-right: 36px;
padding-bottom: 18px;
padding: 24px 64px 20px 28px;
border-bottom: 1px solid var(--border);
background: linear-gradient(180deg, #fff, #fdfdfd);
}
.incident-dialog [data-slot='dialog-header'] .overline {
margin-bottom: 3px;
color: #27845e;
}
.incident-dialog [data-slot='dialog-title'] {
font-size: 19px;
font-size: 21px;
line-height: 1.25;
letter-spacing: -0.025em;
}
.incident-form {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 20px;
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
overflow: hidden;
}
.incident-form > * {
grid-column: 1;
.incident-form-body {
display: grid;
min-height: 0;
padding: 24px 28px 28px;
gap: 22px;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.incident-form-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2fr);
grid-template-columns: minmax(150px, 0.85fr) minmax(150px, 0.85fr) minmax(260px, 1.6fr);
gap: 16px;
}
.incident-form-grid [data-slot='select-trigger'],
.incident-update-status [data-slot='select-trigger'] {
width: 100%;
}
.incident-title-field {
min-width: 0;
}
.incident-title-field input::placeholder {
font-size: 13px;
}
.incident-select-option {
display: inline-flex;
min-width: 0;
@@ -581,7 +609,7 @@
background: #fee9e7;
}
.incident-form textarea {
min-height: 138px;
min-height: 154px;
width: 100%;
resize: vertical;
border: 1px solid var(--border);
@@ -628,6 +656,7 @@
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 52px;
max-height: 104px;
overflow-y: auto;
padding: 10px;
@@ -664,11 +693,23 @@
height: 14px;
accent-color: var(--primary-deep);
}
.incident-service-empty {
display: flex;
align-items: center;
min-height: 30px;
margin: 0;
padding-inline: 2px;
font-size: 13px;
color: var(--muted-foreground);
}
.ai-compose-field {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.ai-compose-field > .form-error {
grid-column: 1 / -1;
}
.ai-compose-pane {
display: flex;
min-width: 0;
@@ -688,6 +729,7 @@
}
.ai-compose-pane-header small {
margin-left: 0;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.ai-compose-button {
@@ -703,23 +745,31 @@
}
.ai-compose-hint button {
border: 0;
padding: 0;
background: transparent;
color: var(--primary);
color: #17865b;
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.incident-form > .form-actions {
align-items: center;
margin-top: 2px;
padding: 0;
border: 0;
background: transparent;
.incident-update-status {
max-width: 260px;
}
.incident-form > .form-actions button {
.incident-dialog-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 0 0 auto;
gap: 10px;
padding: 16px 28px;
border-top: 1px solid var(--border);
background: #fafafa;
}
.incident-dialog-actions button {
min-height: 40px;
padding-inline: 18px;
}
.incident-form > .form-actions .secondary-button {
.incident-dialog-actions .secondary-button {
border-color: #d4d4d4;
color: #444;
background: #fff;
@@ -727,12 +777,12 @@
0 1px 2px rgb(0 0 0 / 0.05),
inset 0 1px rgb(255 255 255 / 0.7);
}
.incident-form > .form-actions .secondary-button:hover:not(:disabled) {
.incident-dialog-actions .secondary-button:hover:not(:disabled) {
border-color: #aaa;
color: var(--foreground);
background: #f5f5f5;
}
.incident-form > .form-actions .primary-button {
.incident-dialog-actions .primary-button {
min-width: 148px;
box-shadow:
0 1px 2px rgb(0 0 0 / 0.08),
@@ -838,7 +888,19 @@
@media (max-width: 640px) {
.incident-dialog {
padding: 22px;
width: calc(100% - 24px);
max-height: calc(100dvh - 24px);
border-radius: 12px;
}
.incident-dialog [data-slot='dialog-header'] {
padding: 21px 58px 18px 20px;
}
.incident-form-body {
padding: 20px;
gap: 20px;
}
.incident-dialog-actions {
padding: 14px 20px;
}
.dashboard-incident-list article {
grid-template-columns: 1fr;
@@ -890,22 +952,50 @@
}
@media (max-width: 440px) {
.incident-dialog {
top: auto;
bottom: 0;
width: 100%;
max-width: none;
max-height: calc(100dvh - 8px);
translate: -50% 0;
border-radius: 16px 16px 0 0;
}
.incident-dialog [data-slot='dialog-header'] {
padding: 20px 52px 17px 18px;
}
.incident-dialog [data-slot='dialog-title'] {
font-size: 20px;
}
.incident-form-body {
padding: 18px;
gap: 18px;
}
.incident-form-grid {
grid-template-columns: 1fr;
}
.incident-title-field {
grid-column: auto;
}
.incident-form > .form-actions {
gap: 8px;
.incident-form textarea {
min-height: 132px;
}
.incident-form > .form-actions button {
.incident-dialog-actions {
gap: 8px;
padding: 12px 18px max(12px, env(safe-area-inset-bottom));
}
.incident-dialog-actions button {
flex: 1;
padding-inline: 12px;
}
.incident-form > .form-actions .primary-button {
.incident-dialog-actions .primary-button {
min-width: 0;
}
.ai-compose-pane-header {
align-items: flex-start;
flex-direction: column;
gap: 2px;
}
}
@media (max-width: 380px) {
@@ -1062,7 +1152,7 @@
}
.dark .metric-card span {
color: #6b7280;
color: var(--faint);
}
/* Services / Monitor List Panel */
@@ -1127,7 +1217,7 @@
}
.dark .monitor-meta {
color: #6b7280;
color: var(--faint);
}
.dark .monitor-result code {
@@ -1234,7 +1324,7 @@
/* Dashboard Page Footer */
.dark .dashboard-page-footer {
border-top-color: rgba(255, 255, 255, 0.08);
color: #6b7280;
color: var(--faint);
}
/* Dialogs & Forms in Dark Mode (Add/Edit Monitor, Incident, Delete) */
@@ -1335,8 +1425,31 @@
}
/* Incident Dialog specifics */
.dark [data-slot='dialog-overlay'] {
background: rgba(0, 0, 0, 0.58);
}
.dark .incident-dialog [data-slot='dialog-header'] {
border-bottom-color: rgba(255, 255, 255, 0.08);
background: linear-gradient(180deg, #111419, #101318);
}
.dark .incident-dialog-actions {
border-top-color: rgba(255, 255, 255, 0.08);
background: #0e1116;
}
.dark .incident-dialog-actions .secondary-button {
border-color: rgba(255, 255, 255, 0.14);
color: #d1d5db;
background: #181c23;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
.dark .incident-dialog-actions .secondary-button:hover:not(:disabled) {
border-color: rgba(255, 255, 255, 0.24);
color: #f9fafb;
background: #20252e;
}
.dark .incident-service-options {
@@ -1361,6 +1474,10 @@
background: rgba(62, 207, 142, 0.12);
}
.dark .incident-service-empty {
color: #9ca3af;
}
.dark .ai-compose-pane-header label {
color: #d1d5db;
}
@@ -1539,7 +1656,7 @@
}
.dark .metric-card span {
color: #6b7280;
color: var(--faint);
}
/* Services / Monitor List Panel */
@@ -1604,7 +1721,7 @@
}
.dark .monitor-meta {
color: #6b7280;
color: var(--faint);
}
.dark .monitor-result code {
@@ -1711,7 +1828,7 @@
/* Dashboard Page Footer */
.dark .dashboard-page-footer {
border-top-color: rgba(255, 255, 255, 0.08);
color: #6b7280;
color: var(--faint);
}
/* Dialogs & Forms in Dark Mode (Add/Edit Monitor, Incident, Delete) */
+259 -259
View File
@@ -1,259 +1,259 @@
.auth-page {
position: relative;
display: grid;
grid-template-rows: auto 1fr auto;
justify-items: center;
min-height: 100dvh;
padding: 32px 24px 24px;
background: radial-gradient(circle at 50% 38%, rgb(62 207 142 / 0.08), transparent 34%), linear-gradient(#fff, #fcfcfc);
transition:
background 0.25s ease,
color 0.25s ease;
}
.dark .auth-page {
background:
radial-gradient(ellipse 75% 55% at 50% 30%, rgba(62, 207, 142, 0.12), transparent 65%),
radial-gradient(ellipse 40% 30% at 50% 12%, rgba(36, 180, 126, 0.07), transparent 50%),
linear-gradient(180deg, #0c0e10 0%, #070809 100%);
}
.auth-theme-toggle {
position: absolute;
z-index: 1;
top: 28px;
right: 28px;
transition:
color 0.15s ease,
background 0.15s ease,
border-color 0.15s ease;
}
.dark .auth-theme-toggle {
color: #9ca3af;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
backdrop-filter: blur(8px);
}
.dark .auth-theme-toggle:hover:not(:disabled) {
color: #3ecf8e;
background: rgba(62, 207, 142, 0.1);
border-color: rgba(62, 207, 142, 0.25);
}
.auth-page::before {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-image: radial-gradient(#dcdcdc 0.65px, transparent 0.65px);
background-size: 16px 16px;
mask-image: linear-gradient(to bottom, transparent, black 24%, transparent 76%);
content: '';
}
.dark .auth-page::before {
background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px);
background-size: 20px 20px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%);
}
.auth-brand {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 20px;
font-weight: 600;
letter-spacing: -0.6px;
}
.dark .auth-brand {
color: #ffffff;
}
.auth-brand-mark {
width: 26px;
height: 26px;
color: var(--primary-deep);
}
.dark .auth-brand-mark {
color: #3ecf8e;
filter: drop-shadow(0 0 12px rgba(62, 207, 142, 0.45));
}
.auth-card {
position: relative;
z-index: 1;
align-self: center;
width: min(100%, 420px);
padding: 36px;
border: 1px solid var(--hairline);
border-radius: 12px;
background: rgb(255 255 255 / 0.96);
box-shadow:
0 18px 55px rgb(24 74 52 / 0.08),
0 2px 8px rgb(0 0 0 / 0.04);
animation: enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.dark .auth-card {
background: rgba(17, 19, 23, 0.82);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 24px 64px -12px rgba(0, 0, 0, 0.8),
0 0 45px -10px rgba(62, 207, 142, 0.08),
inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
}
.auth-heading p {
margin: 0 0 12px;
font:
500 10px/1.4 'IBM Plex Mono',
monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #55816e;
}
.dark .auth-heading p {
color: #3ecf8e;
}
.auth-heading h1 {
margin: 0;
font-size: 30px;
font-weight: 500;
line-height: 1.16;
letter-spacing: -1.2px;
}
.dark .auth-heading h1 {
color: #f9fafb;
}
.auth-heading > span {
display: block;
margin-top: 12px;
font-size: 19px;
line-height: 1.55;
color: #626262;
}
.dark .auth-heading > span {
color: #9ca3af;
}
.auth-form {
display: grid;
gap: 20px;
margin-top: 30px;
}
.auth-field {
display: grid;
gap: 8px;
}
.auth-field label {
font-size: 13px;
font-weight: 500;
color: #353535;
}
.dark .auth-field label {
color: #d1d5db;
}
.dark .auth-field input {
background-color: rgba(11, 13, 15, 0.85);
border-color: rgba(255, 255, 255, 0.12);
color: #f9fafb;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.dark .auth-field input:hover {
border-color: rgba(255, 255, 255, 0.24);
}
.dark .auth-field input:focus-visible {
border-color: #3ecf8e;
box-shadow:
0 0 0 3px rgba(62, 207, 142, 0.2),
inset 0 1px 2px rgba(0, 0, 0, 0.4);
}
.auth-error {
margin: -4px 0 0;
padding: 10px 12px;
border: 1px solid #efcaca;
border-radius: 6px;
font-size: 12px;
line-height: 1.45;
color: #9f2f2f;
background: #fff6f6;
}
.dark .auth-error {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.28);
color: #fca5a5;
}
.auth-submit {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 42px;
padding: 8px 16px;
border: 1px solid #35c586;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
color: var(--ink);
background: var(--primary);
box-shadow:
0 1px 2px rgb(0 0 0 / 0.08),
inset 0 1px rgb(255 255 255 / 0.2);
cursor: pointer;
}
.auth-submit:hover:not(:disabled) {
background: #36c487;
transform: translateY(-1px);
}
.auth-submit:active:not(:disabled) {
background: var(--primary-deep);
transform: translateY(1px);
}
.auth-submit:disabled {
cursor: wait;
opacity: 0.62;
}
.dark .auth-submit {
background: #3ecf8e;
border-color: #3ecf8e;
color: #090a0c;
font-weight: 600;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.3),
0 4px 14px rgba(62, 207, 142, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.35);
transition: all 0.15s ease;
}
.dark .auth-submit:hover:not(:disabled) {
background: #4ae09e;
border-color: #4ae09e;
box-shadow:
0 4px 20px rgba(62, 207, 142, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
transform: translateY(-1px);
}
.dark .auth-submit:active:not(:disabled) {
background: #24b47e;
transform: translateY(1px);
}
.auth-footnote {
position: relative;
z-index: 1;
margin: 0;
font-size: 11px;
color: #858585;
}
.dark .auth-footnote {
color: #6b7280;
}
@media (max-width: 440px) {
.auth-page {
padding: 24px 16px;
}
.auth-card {
padding: 28px 22px;
}
.auth-heading h1 {
font-size: 27px;
}
}
.auth-page {
position: relative;
display: grid;
grid-template-rows: auto 1fr auto;
justify-items: center;
min-height: 100dvh;
padding: 32px 24px 24px;
background: radial-gradient(circle at 50% 38%, rgb(62 207 142 / 0.08), transparent 34%), linear-gradient(#fff, #fcfcfc);
transition:
background 0.25s ease,
color 0.25s ease;
}
.dark .auth-page {
background:
radial-gradient(ellipse 75% 55% at 50% 30%, rgba(62, 207, 142, 0.12), transparent 65%),
radial-gradient(ellipse 40% 30% at 50% 12%, rgba(36, 180, 126, 0.07), transparent 50%),
linear-gradient(180deg, #0c0e10 0%, #070809 100%);
}
.auth-theme-toggle {
position: absolute;
z-index: 1;
top: 28px;
right: 28px;
transition:
color 0.15s ease,
background 0.15s ease,
border-color 0.15s ease;
}
.dark .auth-theme-toggle {
color: #9ca3af;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
backdrop-filter: blur(8px);
}
.dark .auth-theme-toggle:hover:not(:disabled) {
color: #3ecf8e;
background: rgba(62, 207, 142, 0.1);
border-color: rgba(62, 207, 142, 0.25);
}
.auth-page::before {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-image: radial-gradient(#dcdcdc 0.65px, transparent 0.65px);
background-size: 16px 16px;
mask-image: linear-gradient(to bottom, transparent, black 24%, transparent 76%);
content: '';
}
.dark .auth-page::before {
background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px);
background-size: 20px 20px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%);
}
.auth-brand {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 20px;
font-weight: 600;
letter-spacing: -0.6px;
}
.dark .auth-brand {
color: #ffffff;
}
.auth-brand-mark {
width: 26px;
height: 26px;
color: var(--primary-deep);
}
.dark .auth-brand-mark {
color: #3ecf8e;
filter: drop-shadow(0 0 12px rgba(62, 207, 142, 0.45));
}
.auth-card {
position: relative;
z-index: 1;
align-self: center;
width: min(100%, 420px);
padding: 36px;
border: 1px solid var(--hairline);
border-radius: 12px;
background: rgb(255 255 255 / 0.96);
box-shadow:
0 18px 55px rgb(24 74 52 / 0.08),
0 2px 8px rgb(0 0 0 / 0.04);
animation: enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.dark .auth-card {
background: rgba(17, 19, 23, 0.82);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 24px 64px -12px rgba(0, 0, 0, 0.8),
0 0 45px -10px rgba(62, 207, 142, 0.08),
inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
}
.auth-heading p {
margin: 0 0 12px;
font:
500 10px/1.4 'IBM Plex Mono',
monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #55816e;
}
.dark .auth-heading p {
color: #3ecf8e;
}
.auth-heading h1 {
margin: 0;
font-size: 30px;
font-weight: 500;
line-height: 1.16;
letter-spacing: -1.2px;
}
.dark .auth-heading h1 {
color: #f9fafb;
}
.auth-heading > span {
display: block;
margin-top: 12px;
font-size: 19px;
line-height: 1.55;
color: #626262;
}
.dark .auth-heading > span {
color: #9ca3af;
}
.auth-form {
display: grid;
gap: 20px;
margin-top: 30px;
}
.auth-field {
display: grid;
gap: 8px;
}
.auth-field label {
font-size: 13px;
font-weight: 500;
color: #353535;
}
.dark .auth-field label {
color: #d1d5db;
}
.dark .auth-field input {
background-color: rgba(11, 13, 15, 0.85);
border-color: rgba(255, 255, 255, 0.12);
color: #f9fafb;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.dark .auth-field input:hover {
border-color: rgba(255, 255, 255, 0.24);
}
.dark .auth-field input:focus-visible {
border-color: #3ecf8e;
box-shadow:
0 0 0 3px rgba(62, 207, 142, 0.2),
inset 0 1px 2px rgba(0, 0, 0, 0.4);
}
.auth-error {
margin: -4px 0 0;
padding: 10px 12px;
border: 1px solid #efcaca;
border-radius: 6px;
font-size: 12px;
line-height: 1.45;
color: #9f2f2f;
background: #fff6f6;
}
.dark .auth-error {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.28);
color: #fca5a5;
}
.auth-submit {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 42px;
padding: 8px 16px;
border: 1px solid #35c586;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
color: var(--ink);
background: var(--primary);
box-shadow:
0 1px 2px rgb(0 0 0 / 0.08),
inset 0 1px rgb(255 255 255 / 0.2);
cursor: pointer;
}
.auth-submit:hover:not(:disabled) {
background: #36c487;
transform: translateY(-1px);
}
.auth-submit:active:not(:disabled) {
background: var(--primary-deep);
transform: translateY(1px);
}
.auth-submit:disabled {
cursor: wait;
opacity: 0.62;
}
.dark .auth-submit {
background: #3ecf8e;
border-color: #3ecf8e;
color: #090a0c;
font-weight: 600;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.3),
0 4px 14px rgba(62, 207, 142, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.35);
transition: all 0.15s ease;
}
.dark .auth-submit:hover:not(:disabled) {
background: #4ae09e;
border-color: #4ae09e;
box-shadow:
0 4px 20px rgba(62, 207, 142, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
transform: translateY(-1px);
}
.dark .auth-submit:active:not(:disabled) {
background: #24b47e;
transform: translateY(1px);
}
.auth-footnote {
position: relative;
z-index: 1;
margin: 0;
font-size: 11px;
color: #858585;
}
.dark .auth-footnote {
color: var(--faint);
}
@media (max-width: 440px) {
.auth-page {
padding: 24px 16px;
}
.auth-card {
padding: 28px 22px;
}
.auth-heading h1 {
font-size: 27px;
}
}
+6 -6
View File
@@ -1505,7 +1505,7 @@
}
.dark .uptime-days-caption {
color: #6b7280;
color: var(--faint);
}
.dark .uptime-days-caption i {
@@ -1598,7 +1598,7 @@
}
.dark .past-incident-meta {
color: #6b7280;
color: var(--faint);
}
.dark .past-incident-chevron {
@@ -1634,7 +1634,7 @@
/* Status Footer */
.dark .status-footer {
border-top-color: rgba(255, 255, 255, 0.08);
color: #6b7280;
color: var(--faint);
}
.dark .status-footer i {
@@ -1676,7 +1676,7 @@
}
.dark .incident-detail-summary dt {
color: #6b7280;
color: var(--faint);
}
.dark .incident-detail-summary dd {
@@ -1692,7 +1692,7 @@
}
.dark .incident-timeline-heading > span {
color: #6b7280;
color: var(--faint);
}
.dark .incident-timeline strong {
@@ -1700,7 +1700,7 @@
}
.dark .incident-timeline time {
color: #6b7280;
color: var(--faint);
}
.dark .incident-timeline-entry p {
+5 -5
View File
@@ -6,11 +6,11 @@ import { cn } from '@/lib/utils';
const badgeVariants = cva('inline-flex items-center gap-1.75 text-[12px] font-medium [&>i]:size-1.75 [&>i]:shrink-0 [&>i]:rounded-full', {
variants: {
variant: {
online: 'text-[#16885b] [&>i]:bg-(--primary-deep) [&>i]:shadow-[0_0_0_3px_rgb(62_207_142/0.12)]',
offline: 'text-[#ae3d3d] [&>i]:bg-[#d05a5a]',
pending: 'text-[#8a5a1f] [&>i]:bg-[#e0913f] [&>i]:shadow-[0_0_0_3px_rgb(224_145_63/0.14)]',
checking: 'text-[#8b7722] [&>i]:animate-[blink_1.1s_ease-in-out_infinite] [&>i]:bg-[#d7bd53]',
maintenance: 'text-[#3f6fb8] [&>i]:bg-[#5a8fd0]',
online: 'text-[#16885b] dark:text-[#4ade80] [&>i]:bg-(--primary-deep) [&>i]:shadow-[0_0_0_3px_rgb(62_207_142/0.12)]',
offline: 'text-[#ae3d3d] dark:text-[#f87171] [&>i]:bg-[#d05a5a]',
pending: 'text-[#8a5a1f] dark:text-[#f6c86a] [&>i]:bg-[#e0913f] [&>i]:shadow-[0_0_0_3px_rgb(224_145_63/0.14)]',
checking: 'text-[#8b7722] dark:text-[#fde047] [&>i]:animate-[blink_1.1s_ease-in-out_infinite] [&>i]:bg-[#d7bd53]',
maintenance: 'text-[#3f6fb8] dark:text-[#8bb5ee] [&>i]:bg-[#5a8fd0]',
},
},
defaultVariants: {
+10 -1
View File
@@ -7,7 +7,16 @@ import { cn } from '@/lib/utils';
function Card({ className, asChild = false, ...props }: React.ComponentProps<'div'> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : 'div';
return <Comp data-slot="card" className={cn('flex flex-col rounded-[8px] border border-[#e3e3e3] bg-white', className)} {...props} />;
return (
<Comp
data-slot="card"
className={cn(
'flex flex-col rounded-[8px] border border-[#e3e3e3] bg-white text-(--ink) dark:border-white/10 dark:bg-[#111419]',
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+6 -4
View File
@@ -7,7 +7,7 @@ const emptyVariants = cva('grid justify-items-center bg-transparent p-[56px_24px
variants: {
variant: {
default: '',
error: 'rounded-[8px] border border-[#ebd1d1] bg-[#fffafa]',
error: 'rounded-[8px] border border-[#ebd1d1] bg-[#fffafa] dark:border-red-400/25 dark:bg-red-400/6',
},
},
defaultVariants: {
@@ -19,7 +19,7 @@ const emptyMediaVariants = cva('', {
variants: {
variant: {
default: '',
icon: 'mb-4.5 grid size-12 place-items-center rounded-[8px] border border-[#dcdcdc] bg-[#fafafa] text-[#666] [&_svg]:size-5.5',
icon: 'mb-4.5 grid size-12 place-items-center rounded-[8px] border border-[#dcdcdc] bg-[#fafafa] text-[#666] dark:border-white/12 dark:bg-white/4 dark:text-[#aeb4bf] [&_svg]:size-5.5',
},
},
defaultVariants: {
@@ -59,7 +59,9 @@ function EmptyMedia({ className, variant, ...props }: React.ComponentProps<'div'
data-variant={variant}
className={cn(
emptyMediaVariants({ variant }),
emptyVariant === 'error' && variant === 'icon' && 'border-[#ebcaca] bg-[#fff3f3] text-[#a34242]',
emptyVariant === 'error' &&
variant === 'icon' &&
'border-[#ebcaca] bg-[#fff3f3] text-[#a34242] dark:border-red-400/30 dark:bg-red-400/10 dark:text-red-300',
className,
)}
{...props}
@@ -73,7 +75,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="empty-title"
className={cn('text-[16px] font-medium text-[#171717]', variant === 'error' && 'text-[#8b3434]', className)}
className={cn('text-[16px] font-medium text-(--ink)', variant === 'error' && 'text-[#8b3434] dark:text-red-300', className)}
{...props}
/>
);
+2 -2
View File
@@ -12,10 +12,10 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
type={type}
data-slot="input"
className={cn(
'flex min-h-10.5 w-full rounded-[6px] border border-[#cfcfcf] bg-white px-3 py-2',
'flex min-h-10.5 w-full rounded-[6px] border border-[#cfcfcf] bg-white px-3 py-2 dark:border-white/12 dark:bg-[#0b0d0f]',
'text-[14px] text-(--ink) shadow-[inset_0_1px_2px_rgb(0_0_0/0.025)] transition-colors outline-none',
'placeholder:text-(--faint)',
'hover:border-[#aaa]',
'hover:border-[#aaa] dark:hover:border-white/22',
'focus-visible:border-(--primary-deep) focus-visible:shadow-[0_0_0_3px_rgb(36_180_126/0.14)] focus-visible:outline-none!',
'disabled:cursor-not-allowed disabled:opacity-50',
'aria-invalid:border-destructive aria-invalid:shadow-[0_0_0_3px_rgb(220_98_98/0.14)]',
+4 -4
View File
@@ -25,9 +25,9 @@ function SelectTrigger({ className, children, ...props }: React.ComponentProps<t
<SelectPrimitive.Trigger
data-slot="select-trigger"
className={cn(
'group flex min-h-10.5 w-full cursor-pointer items-center justify-between gap-2 rounded-[6px] border border-[#cfcfcf] bg-white px-3 py-2 text-left',
'group flex min-h-10.5 w-full cursor-pointer items-center justify-between gap-2 rounded-[6px] border border-[#cfcfcf] bg-white px-3 py-2 text-left dark:border-white/12 dark:bg-[#0b0d0f]',
'text-[14px] text-(--ink) shadow-[inset_0_1px_2px_rgb(0_0_0/0.025)] transition-colors outline-none data-placeholder:text-(--faint) [&_svg]:shrink-0',
'hover:border-[#aaa]',
'hover:border-[#aaa] dark:hover:border-white/22',
'focus-visible:border-(--primary-deep) focus-visible:shadow-[0_0_0_3px_rgb(36_180_126/0.14)] focus-visible:outline-none!',
'data-[state=open]:border-(--primary-deep) data-[state=open]:shadow-[0_0_0_3px_rgb(36_180_126/0.14)]',
'disabled:cursor-not-allowed disabled:opacity-50',
@@ -51,7 +51,7 @@ function SelectContent({ className, children, position = 'popper', ...props }: R
data-slot="select-content"
className={cn(
'relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto',
'rounded-[8px] border border-(--hairline) bg-white text-(--ink) shadow-[0_8px_24px_rgb(0_0_0/0.08)]',
'rounded-[8px] border border-(--hairline) bg-white text-(--ink) shadow-[0_8px_24px_rgb(0_0_0/0.08)] dark:bg-[#111419] dark:shadow-[0_12px_32px_rgb(0_0_0/0.55)]',
'duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
'data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1',
position === 'popper' &&
@@ -87,7 +87,7 @@ function SelectItem({ className, children, ...props }: React.ComponentProps<type
data-slot="select-item"
className={cn(
'relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-[14px] text-(--ink) outline-hidden select-none',
'focus:bg-[#ededed] data-[state=checked]:font-medium',
'focus:bg-[#ededed] dark:focus:bg-white/8 data-[state=checked]:font-medium',
'data-disabled:pointer-events-none data-disabled:opacity-50',
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
+9 -3
View File
@@ -26,7 +26,10 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return (
<th
data-slot="table-head"
className={cn('sticky top-0 z-1 bg-[#fafafa] px-4 py-2.75 text-left font-normal text-[#888] shadow-[0_1px_#e5e5e5]', className)}
className={cn(
'sticky top-0 z-1 bg-[#fafafa] px-4 py-2.75 text-left font-normal text-[#707070] shadow-[0_1px_#e5e5e5] dark:bg-[#14171d] dark:text-[#aeb4bf] dark:shadow-[0_1px_rgb(255_255_255/0.08)]',
className,
)}
{...props}
/>
);
@@ -36,14 +39,17 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return (
<td
data-slot="table-cell"
className={cn('whitespace-nowrap border-t border-[#ededed] px-4 py-3.5 text-[#555]', className)}
className={cn(
'whitespace-nowrap border-t border-[#ededed] px-4 py-3.5 text-[#555] dark:border-white/8 dark:text-[#d1d5db]',
className,
)}
{...props}
/>
);
}
function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) {
return <caption data-slot="table-caption" className={cn('mt-4 text-[12px] text-[#888]', className)} {...props} />;
return <caption data-slot="table-caption" className={cn('mt-4 text-[12px] text-[#707070] dark:text-[#aeb4bf]', className)} {...props} />;
}
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };