d728af53b1
Closes #17. Everything a team owner configures was API-only: escalation, integrations, dead man's switches, membership, and the rota -- which the on-call view still described as the TUI's job, and the TUI has been broken against this server since teams landed. Setting up the feature this whole line of work exists for meant using curl. A Team tab now holds all of it, one team at a time, with a picker for somebody in more than one. An owner edits; a member sees the same page without the controls, because the server refuses their writes anyway -- hiding a button is a courtesy to the reader, not the thing enforcing anything. The escalation editor holds a draft and sends the whole ladder, because the API replaces it wholesale: the levels are an order, and patching one rung leaves the numbering of the others undecided. Adding a level defaults to five minutes and the rota, which is the shape almost every ladder starts as. An integration key is returned exactly once, so creating one opens a panel that says so, shows the URL large with a copy button, and renders the Alertmanager receiver snippet with the URL already in it -- the next thing anybody does with that key is paste it into a config. The panel stays until it is dismissed rather than disappearing on the next re-render. The incident view gains where an incident is on the ladder and when the next page is due, which is the question somebody looking at an unacknowledged incident actually has. The API carries it: the incident payload now includes escalation_level and escalation_due_at, the latter computed in the incident SELECT by joining the level's timeout, so a list costs no extra queries. Verified against a live server by making every call the page makes, including the writes: the six reads the Team tab issues, a two-level ladder saved and read back, an integration created and its key returned once, three days of rota assigned, switches set, and an incident showing level 1 with a due time five minutes out. Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
690 lines
27 KiB
CSS
690 lines
27 KiB
CSS
/* terdut web UI.
|
|
*
|
|
* Mobile first: one column, a top bar and a bottom tab bar. From 900px the tab
|
|
* bar becomes a sidebar and the queue shows list and detail side by side.
|
|
* Colour is reserved for severity and status; everything else is neutral.
|
|
*/
|
|
|
|
:root {
|
|
--bg: #f5f6f8;
|
|
--surface: #ffffff;
|
|
--surface-2: #eff1f4;
|
|
--surface-hover: #f7f8fa;
|
|
--border: #e2e5ea;
|
|
--border-strong: #cfd3da;
|
|
--text: #16181d;
|
|
--muted: #5b626e;
|
|
--faint: #8a909b;
|
|
|
|
--accent: #2f5bd3;
|
|
--accent-text: #ffffff;
|
|
--accent-soft: #e8eefc;
|
|
|
|
--crit: #d0342c;
|
|
--crit-soft: #fdecea;
|
|
--warn: #b86e00;
|
|
--warn-soft: #fdf3e1;
|
|
--info: #2f6fdf;
|
|
--info-soft: #e9f0fd;
|
|
--ok: #1d7f4c;
|
|
--ok-soft: #e6f5ec;
|
|
--snooze: #6b5bd2;
|
|
--snooze-soft: #efedfb;
|
|
|
|
--radius: 10px;
|
|
--radius-sm: 6px;
|
|
--shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 8%);
|
|
--shadow-lg: 0 12px 32px rgb(16 24 40 / 18%);
|
|
|
|
--font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
|
|
--topbar-h: 52px;
|
|
--tabbar-h: 58px;
|
|
--safe-top: env(safe-area-inset-top, 0px);
|
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #0f1115;
|
|
--surface: #171a20;
|
|
--surface-2: #1f232b;
|
|
--surface-hover: #1c2027;
|
|
--border: #2a2f38;
|
|
--border-strong: #394050;
|
|
--text: #e7e9ed;
|
|
--muted: #a0a7b3;
|
|
--faint: #737a87;
|
|
|
|
--accent: #6d8ff0;
|
|
--accent-text: #0b0d12;
|
|
--accent-soft: #1d2640;
|
|
|
|
--crit: #ff6b61;
|
|
--crit-soft: #3a1c1b;
|
|
--warn: #f0b140;
|
|
--warn-soft: #362a14;
|
|
--info: #74a3ff;
|
|
--info-soft: #1a2640;
|
|
--ok: #4cc488;
|
|
--ok-soft: #15301f;
|
|
--snooze: #a89bff;
|
|
--snooze-soft: #262245;
|
|
|
|
--shadow: 0 1px 2px rgb(0 0 0 / 40%);
|
|
--shadow-lg: 0 16px 40px rgb(0 0 0 / 55%);
|
|
}
|
|
}
|
|
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
[hidden] { display: none !important; }
|
|
|
|
html { -webkit-text-size-adjust: 100%; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font: 15px/1.45 var(--font);
|
|
font-variant-numeric: tabular-nums;
|
|
-webkit-font-smoothing: antialiased;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
a { color: inherit; text-decoration: none; }
|
|
button, input, textarea, select { font: inherit; color: inherit; }
|
|
code { font-family: var(--mono); font-size: 0.9em; }
|
|
h1, h2, h3 { margin: 0; line-height: 1.25; }
|
|
|
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
|
|
/* ---------- boot + login ---------- */
|
|
|
|
.boot { display: grid; place-items: center; min-height: 100dvh; }
|
|
.spinner {
|
|
width: 26px; height: 26px; border-radius: 50%;
|
|
border: 3px solid var(--border); border-top-color: var(--accent);
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
.login {
|
|
min-height: 100dvh;
|
|
display: grid; place-items: center;
|
|
padding: calc(24px + var(--safe-top)) 16px calc(24px + var(--safe-bottom));
|
|
}
|
|
.login-card {
|
|
width: 100%; max-width: 360px;
|
|
display: grid; gap: 14px;
|
|
}
|
|
.login-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
.login-brand h1 { font-size: 24px; letter-spacing: -0.01em; }
|
|
.login-hint { color: var(--faint); font-size: 13px; margin: 4px 0 0; }
|
|
|
|
label { display: grid; gap: 6px; }
|
|
label > span { font-size: 13px; font-weight: 600; color: var(--muted); }
|
|
|
|
input, textarea, select {
|
|
width: 100%;
|
|
min-height: 44px;
|
|
padding: 10px 12px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 16px; /* 16px stops iOS zooming into the field */
|
|
}
|
|
textarea { min-height: 110px; resize: vertical; line-height: 1.45; }
|
|
input:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
|
|
.form-error {
|
|
margin: 0; padding: 10px 12px;
|
|
background: var(--crit-soft); color: var(--crit);
|
|
border-radius: var(--radius-sm); font-size: 14px;
|
|
}
|
|
|
|
/* ---------- buttons ---------- */
|
|
|
|
.btn {
|
|
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
min-height: 44px; padding: 0 16px;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--surface);
|
|
font-weight: 600; font-size: 15px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: background 0.12s, border-color 0.12s, opacity 0.12s;
|
|
}
|
|
.btn:hover { background: var(--surface-hover); }
|
|
.btn:disabled { opacity: 0.55; cursor: default; }
|
|
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
.btn-primary:hover { background: var(--accent); filter: brightness(1.06); }
|
|
.btn-danger { background: var(--crit); border-color: var(--crit); color: #fff; }
|
|
.btn-danger:hover { background: var(--crit); filter: brightness(1.06); }
|
|
.btn-ghost { background: transparent; border-color: transparent; }
|
|
.btn-ghost:hover { background: var(--surface-2); }
|
|
.btn-block { width: 100%; }
|
|
.btn-icon { width: 44px; padding: 0; }
|
|
.btn-sm { min-height: 32px; padding: 0 8px; font-size: 13px; }
|
|
.btn-sm svg { width: 16px; height: 16px; }
|
|
.btn svg, .icon { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
|
|
/* ---------- app frame ---------- */
|
|
|
|
.app { min-height: 100dvh; }
|
|
|
|
.topbar {
|
|
position: sticky; top: 0; z-index: 10;
|
|
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
height: calc(var(--topbar-h) + var(--safe-top));
|
|
padding: var(--safe-top) 16px 0;
|
|
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
backdrop-filter: saturate(1.4) blur(12px);
|
|
-webkit-backdrop-filter: saturate(1.4) blur(12px);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.topbar-title { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
|
|
|
|
.open-pill {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 3px 10px; border-radius: 999px;
|
|
font-size: 13px; font-weight: 600;
|
|
background: var(--surface-2); color: var(--muted);
|
|
}
|
|
.open-pill::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
|
|
.open-pill.has-triggered { background: var(--crit-soft); color: var(--crit); }
|
|
.open-pill.has-triggered::before { background: var(--crit); }
|
|
.open-pill.all-acked::before { background: var(--warn); }
|
|
|
|
/* Bottom tab bar on phones. */
|
|
.nav {
|
|
position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
|
|
display: grid; grid-template-columns: repeat(4, 1fr);
|
|
height: calc(var(--tabbar-h) + var(--safe-bottom));
|
|
padding-bottom: var(--safe-bottom);
|
|
background: color-mix(in srgb, var(--surface) 92%, transparent);
|
|
backdrop-filter: saturate(1.4) blur(12px);
|
|
-webkit-backdrop-filter: saturate(1.4) blur(12px);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.nav-brand { display: none; }
|
|
.nav-link {
|
|
position: relative;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
|
|
color: var(--faint); font-size: 11px; font-weight: 600;
|
|
}
|
|
.nav-link svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
.nav-link[aria-current="page"] { color: var(--accent); }
|
|
.nav-badge {
|
|
position: absolute; top: 6px; left: calc(50% + 6px);
|
|
min-width: 18px; height: 18px; padding: 0 5px;
|
|
border-radius: 999px; background: var(--crit); color: #fff;
|
|
font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
|
|
}
|
|
|
|
.view { padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom)); }
|
|
.view-page { padding-left: 16px; padding-right: 16px; }
|
|
.view-page > * { max-width: 760px; margin-left: auto; margin-right: auto; }
|
|
|
|
/* Phone detail: the incident takes the whole screen with its own action bar,
|
|
so the tab bar and top bar step aside. */
|
|
.app.detail-open .nav,
|
|
.app.detail-open .topbar { display: none; }
|
|
.app.detail-open .pane-list { display: none; }
|
|
.app.detail-open .view-queue { padding-bottom: 0; }
|
|
.view-queue:not(.has-detail) .pane-detail { display: none; }
|
|
|
|
/* ---------- chips ---------- */
|
|
|
|
.chips {
|
|
display: flex; gap: 6px;
|
|
padding: 12px 16px 8px;
|
|
overflow-x: auto; scrollbar-width: none;
|
|
}
|
|
.chips::-webkit-scrollbar { display: none; }
|
|
.chip {
|
|
flex: none;
|
|
min-height: 34px; padding: 0 12px;
|
|
border: 1px solid var(--border-strong); border-radius: 999px;
|
|
background: var(--surface); color: var(--muted);
|
|
font-size: 13px; font-weight: 600; cursor: pointer;
|
|
}
|
|
.chip[aria-selected="true"] { background: var(--text); border-color: var(--text); color: var(--bg); }
|
|
.chip .count { margin-left: 4px; opacity: 0.7; }
|
|
|
|
/* ---------- lists ---------- */
|
|
|
|
.list { padding: 0 16px 16px; display: grid; gap: 8px; }
|
|
|
|
.row {
|
|
position: relative;
|
|
display: grid; grid-template-columns: 1fr auto; gap: 4px 12px;
|
|
padding: 11px 14px 11px 18px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
}
|
|
.row:hover { background: var(--surface-hover); }
|
|
.row[aria-current="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
.row.kbd-focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
.row::before {
|
|
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
|
|
background: var(--sev, var(--border-strong));
|
|
}
|
|
.row-title {
|
|
font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.row-age { color: var(--faint); font-size: 13px; text-align: right; white-space: nowrap; }
|
|
.row-meta {
|
|
grid-column: 1 / -1;
|
|
display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px;
|
|
color: var(--muted); font-size: 13px;
|
|
min-width: 0;
|
|
}
|
|
.row-meta .labels { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%; color: var(--faint); }
|
|
/* Which team's queue a row came from. Only rendered for somebody in more than
|
|
one team, so it never repeats the same word down the whole list. */
|
|
/* Separates the status chips from the team chips in the queue's filter row. */
|
|
.chip-sep { width: 1px; align-self: stretch; background: var(--border); margin: 0 2px; }
|
|
|
|
.row-team {
|
|
padding: 1px 6px; border-radius: 4px;
|
|
background: var(--surface-2); border: 1px solid var(--border);
|
|
color: var(--muted); font-size: 12px; white-space: nowrap;
|
|
}
|
|
.row.resolved .row-title { color: var(--muted); }
|
|
|
|
.sev-critical { --sev: var(--crit); }
|
|
.sev-warning { --sev: var(--warn); }
|
|
.sev-info { --sev: var(--info); }
|
|
|
|
.empty {
|
|
padding: 48px 16px; text-align: center; color: var(--muted);
|
|
}
|
|
.empty strong { display: block; color: var(--text); font-size: 16px; margin-bottom: 4px; }
|
|
.empty .icon { width: 36px; height: 36px; color: var(--ok); margin-bottom: 8px; }
|
|
|
|
.load-error {
|
|
margin: 12px 16px; padding: 10px 12px;
|
|
background: var(--crit-soft); color: var(--crit);
|
|
border-radius: var(--radius-sm); font-size: 14px;
|
|
}
|
|
|
|
/* ---------- badges ---------- */
|
|
|
|
.badge {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 1px 8px; border-radius: 999px;
|
|
font-size: 12px; font-weight: 700; letter-spacing: 0.01em;
|
|
background: var(--surface-2); color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
.badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
|
.badge.plain::before { display: none; }
|
|
.badge.st-triggered, .badge.st-firing { background: var(--crit-soft); color: var(--crit); }
|
|
.badge.st-acknowledged { background: var(--warn-soft); color: var(--warn); }
|
|
.badge.st-snoozed { background: var(--snooze-soft); color: var(--snooze); }
|
|
.badge.st-resolved { background: var(--ok-soft); color: var(--ok); }
|
|
.badge.sev-critical { background: var(--crit-soft); color: var(--crit); }
|
|
.badge.sev-warning { background: var(--warn-soft); color: var(--warn); }
|
|
.badge.sev-info { background: var(--info-soft); color: var(--info); }
|
|
|
|
/* ---------- incident detail ---------- */
|
|
|
|
.detail { padding: 0 16px calc(96px + var(--safe-bottom)); }
|
|
.detail-head {
|
|
position: sticky; top: 0; z-index: 5;
|
|
display: flex; align-items: center; gap: 4px;
|
|
height: calc(var(--topbar-h) + var(--safe-top));
|
|
margin: 0 -16px; padding: var(--safe-top) 8px 0;
|
|
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
backdrop-filter: saturate(1.4) blur(12px);
|
|
-webkit-backdrop-filter: saturate(1.4) blur(12px);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.detail-head .crumb { font-weight: 600; color: var(--muted); font-size: 14px; }
|
|
.detail-title { font-size: 21px; font-weight: 750; letter-spacing: -0.01em; margin: 16px 0 8px; overflow-wrap: anywhere; }
|
|
.detail-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.card + .card, .section + .section { margin-top: 14px; }
|
|
.card-pad { padding: 14px; }
|
|
|
|
.facts { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; margin: 0; padding: 14px; font-size: 14px; }
|
|
.facts dt { color: var(--muted); }
|
|
.facts dd { margin: 0; overflow-wrap: anywhere; }
|
|
.facts .sub { color: var(--faint); }
|
|
|
|
.section { margin-top: 22px; }
|
|
.section-title {
|
|
display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
|
|
font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
|
|
color: var(--muted); margin: 0 2px 8px;
|
|
}
|
|
|
|
.labels-wrap { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.label {
|
|
display: inline-flex; max-width: 100%;
|
|
font-family: var(--mono); font-size: 12px;
|
|
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
overflow: hidden;
|
|
}
|
|
.label > span { padding: 2px 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.label > span:first-child { background: var(--surface-2); color: var(--muted); }
|
|
|
|
.alert-item { padding: 12px 14px; display: grid; gap: 6px; }
|
|
.alert-item + .alert-item { border-top: 1px solid var(--border); }
|
|
.alert-item-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
.alert-item-name { font-weight: 650; overflow-wrap: anywhere; }
|
|
.alert-item-summary { color: var(--muted); font-size: 14px; overflow-wrap: anywhere; }
|
|
.alert-item-foot { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 13px; color: var(--faint); }
|
|
.alert-item-foot a { color: var(--accent); font-weight: 600; }
|
|
details > summary { cursor: pointer; color: var(--muted); font-size: 13px; font-weight: 600; list-style: none; }
|
|
details > summary::-webkit-details-marker { display: none; }
|
|
details > summary::before { content: "▸ "; }
|
|
details[open] > summary::before { content: "▾ "; }
|
|
details[open] > summary { margin-bottom: 8px; }
|
|
|
|
.timeline { list-style: none; margin: 0; padding: 4px 0; }
|
|
.tl-item {
|
|
position: relative;
|
|
display: grid; grid-template-columns: 20px 1fr; gap: 10px;
|
|
padding: 8px 14px;
|
|
}
|
|
.tl-item::before {
|
|
content: ""; position: absolute; left: 23px; top: 0; bottom: 0; width: 2px; background: var(--border);
|
|
}
|
|
.tl-item:first-child::before { top: 16px; }
|
|
.tl-item:last-child::before { bottom: calc(100% - 16px); }
|
|
.tl-dot {
|
|
position: relative; z-index: 1;
|
|
width: 10px; height: 10px; margin: 5px 0 0 5px; border-radius: 50%;
|
|
background: var(--surface); border: 2px solid var(--faint);
|
|
}
|
|
.tl-triggered .tl-dot, .tl-notify_failed .tl-dot, .tl-deadman_silent .tl-dot { border-color: var(--crit); background: var(--crit); }
|
|
.tl-acknowledged .tl-dot { border-color: var(--warn); background: var(--warn); }
|
|
.tl-resolved .tl-dot { border-color: var(--ok); background: var(--ok); }
|
|
.tl-snoozed .tl-dot { border-color: var(--snooze); }
|
|
.tl-note .tl-dot { border-color: var(--accent); background: var(--accent); }
|
|
.tl-body { min-width: 0; font-size: 14px; }
|
|
.tl-text { overflow-wrap: anywhere; }
|
|
.tl-text .who { font-weight: 650; }
|
|
.tl-time { color: var(--faint); font-size: 12px; }
|
|
.tl-note .note {
|
|
margin-top: 6px; padding: 10px 12px;
|
|
background: var(--surface-2); border-radius: var(--radius-sm);
|
|
white-space: pre-wrap; overflow-wrap: anywhere;
|
|
}
|
|
.note-actions { display: flex; justify-content: flex-end; }
|
|
.note-actions .btn { color: var(--muted); }
|
|
|
|
/* The action bar sits at the bottom of the screen on a phone, and at the
|
|
bottom of the detail pane on desktop. */
|
|
.actionbar {
|
|
position: fixed; left: 0; right: 0; bottom: 0; z-index: 15;
|
|
display: flex; gap: 8px;
|
|
padding: 10px 16px calc(10px + var(--safe-bottom));
|
|
background: var(--surface);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.actionbar .btn { min-height: 48px; }
|
|
.actionbar .btn-primary { flex: 1; font-size: 16px; }
|
|
|
|
.detail-placeholder {
|
|
display: grid; place-items: center; height: 100%;
|
|
color: var(--faint); text-align: center; padding: 32px;
|
|
}
|
|
|
|
/* ---------- sheet (dialog) ---------- */
|
|
|
|
.sheet {
|
|
width: 100%; max-width: 100%;
|
|
max-height: 88dvh;
|
|
margin: auto 0 0; padding: 0;
|
|
border: 0; border-radius: 16px 16px 0 0;
|
|
background: var(--surface); color: var(--text);
|
|
box-shadow: var(--shadow-lg);
|
|
overflow: auto;
|
|
}
|
|
.sheet::backdrop { background: rgb(0 0 0 / 40%); }
|
|
.sheet[open] { animation: sheet-up 0.18s ease-out; }
|
|
@keyframes sheet-up { from { transform: translateY(24px); opacity: 0.6; } }
|
|
.sheet-inner { padding: 8px 16px calc(16px + var(--safe-bottom)); }
|
|
.sheet-grab { width: 40px; height: 4px; margin: 0 auto 12px; border-radius: 2px; background: var(--border-strong); }
|
|
.sheet-title { font-size: 17px; font-weight: 700; margin: 0 0 4px; }
|
|
.sheet-text { color: var(--muted); margin: 0 0 14px; font-size: 14px; }
|
|
.sheet-form { display: grid; gap: 12px; }
|
|
.sheet-actions { display: flex; gap: 8px; margin-top: 16px; }
|
|
.sheet-actions .btn { flex: 1; }
|
|
|
|
.menu { list-style: none; margin: 0 -4px; padding: 0; }
|
|
.menu-item {
|
|
display: flex; align-items: center; gap: 12px;
|
|
width: 100%; min-height: 50px; padding: 0 12px;
|
|
background: none; border: 0; border-radius: var(--radius-sm);
|
|
text-align: left; font-size: 16px; cursor: pointer;
|
|
}
|
|
.menu-item:hover { background: var(--surface-2); }
|
|
.menu-item .icon { color: var(--muted); flex: none; }
|
|
.menu-item .menu-sub { margin-left: auto; color: var(--faint); font-size: 13px; }
|
|
.menu-item.danger, .menu-item.danger .icon { color: var(--crit); }
|
|
.menu-item[aria-checked="true"] { font-weight: 700; }
|
|
.menu-item[aria-checked="true"]::after { content: "✓"; margin-left: 8px; color: var(--accent); }
|
|
.menu-sep { height: 1px; background: var(--border); margin: 6px 12px; }
|
|
|
|
/* ---------- toast ---------- */
|
|
|
|
.toast {
|
|
position: fixed; left: 50%; z-index: 50;
|
|
bottom: calc(var(--tabbar-h) + var(--safe-bottom) + 12px);
|
|
transform: translateX(-50%);
|
|
max-width: calc(100% - 32px);
|
|
padding: 10px 16px; border-radius: var(--radius);
|
|
background: var(--text); color: var(--bg);
|
|
font-size: 14px; font-weight: 600;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
.toast.error { background: var(--crit); color: #fff; }
|
|
.app.detail-open ~ .toast { bottom: calc(80px + var(--safe-bottom)); }
|
|
|
|
/* ---------- on-call ---------- */
|
|
|
|
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 16px auto 12px; }
|
|
.page-head h2 { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
|
|
|
|
.now-card { display: flex; align-items: center; gap: 14px; padding: 16px; margin-top: 16px; }
|
|
.avatar {
|
|
flex: none; display: grid; place-items: center;
|
|
width: 44px; height: 44px; border-radius: 50%;
|
|
background: var(--accent-soft); color: var(--accent);
|
|
font-weight: 750; font-size: 17px; text-transform: uppercase;
|
|
}
|
|
.avatar.none { background: var(--surface-2); color: var(--faint); }
|
|
.now-label { color: var(--muted); font-size: 13px; font-weight: 600; }
|
|
.now-name { font-size: 20px; font-weight: 750; }
|
|
.you { color: var(--accent); font-weight: 650; font-size: 13px; margin-left: 6px; }
|
|
|
|
.week-nav { display: flex; align-items: center; gap: 4px; }
|
|
.week-nav .label { font-size: 14px; font-weight: 650; min-width: 9em; text-align: center; }
|
|
.days { list-style: none; margin: 0; padding: 0; }
|
|
.day { display: grid; grid-template-columns: 3.2em 4.2em 1fr; align-items: center; gap: 8px; min-height: 50px; padding: 0 14px; }
|
|
.day + .day { border-top: 1px solid var(--border); }
|
|
.day-name { font-weight: 650; }
|
|
.day-date { color: var(--faint); font-size: 13px; }
|
|
.day-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.day-who.nobody { color: var(--faint); font-style: italic; }
|
|
.day.today { background: var(--accent-soft); }
|
|
.day.today:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
.day.today:last-child { border-radius: 0 0 var(--radius) var(--radius); }
|
|
.day.today .day-name { color: var(--accent); }
|
|
.day.past { opacity: 0.6; }
|
|
|
|
.shift-list { list-style: none; margin: 0; padding: 0; }
|
|
.shift-list li { display: flex; justify-content: space-between; padding: 12px 14px; }
|
|
.shift-list li + li { border-top: 1px solid var(--border); }
|
|
.shift-list .muted { color: var(--faint); }
|
|
|
|
/* ---------- alerts page ---------- */
|
|
|
|
.view-alerts .chips { padding-left: 0; padding-right: 0; }
|
|
.view-alerts .list { padding-left: 0; padding-right: 0; }
|
|
.row.st-firing { --sev: var(--crit); }
|
|
.row.st-resolved { --sev: var(--ok); }
|
|
.row.no-link { cursor: default; }
|
|
|
|
/* ---------- account ---------- */
|
|
|
|
.account-card { display: flex; align-items: center; gap: 14px; padding: 16px; margin-top: 16px; }
|
|
.account-name { font-size: 18px; font-weight: 750; }
|
|
.account-email { color: var(--muted); font-size: 14px; overflow-wrap: anywhere; }
|
|
.pw-form { display: grid; gap: 12px; padding: 16px; }
|
|
.form-ok {
|
|
margin: 0; padding: 10px 12px;
|
|
background: var(--ok-soft); color: var(--ok);
|
|
border-radius: var(--radius-sm); font-size: 14px;
|
|
}
|
|
.kbd-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
.kbd-table td { padding: 8px 14px; border-top: 1px solid var(--border); }
|
|
.kbd-table tr:first-child td { border-top: 0; }
|
|
kbd {
|
|
display: inline-block; min-width: 1.6em; padding: 1px 6px;
|
|
font-family: var(--mono); font-size: 12px; text-align: center;
|
|
background: var(--surface-2); border: 1px solid var(--border-strong); border-bottom-width: 2px;
|
|
border-radius: 4px;
|
|
}
|
|
.only-desktop { display: none; }
|
|
.foot-note { color: var(--faint); font-size: 13px; text-align: center; margin: 24px auto; }
|
|
|
|
/* ---------- desktop ---------- */
|
|
|
|
@media (min-width: 900px) {
|
|
:root { --tabbar-h: 0px; }
|
|
|
|
.app { display: grid; grid-template-columns: 220px 1fr; height: 100dvh; }
|
|
|
|
.nav {
|
|
position: static; grid-row: 1 / span 2;
|
|
display: flex; flex-direction: column; gap: 2px;
|
|
height: auto; padding: 16px 12px;
|
|
background: var(--surface);
|
|
border-top: 0; border-right: 1px solid var(--border);
|
|
backdrop-filter: none;
|
|
}
|
|
.nav-brand {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 4px 10px 18px; font-size: 18px; font-weight: 750; letter-spacing: -0.01em;
|
|
}
|
|
.nav-link {
|
|
flex-direction: row; justify-content: flex-start; gap: 12px;
|
|
min-height: 40px; padding: 0 10px; border-radius: var(--radius-sm);
|
|
color: var(--muted); font-size: 14px;
|
|
}
|
|
.nav-link:hover { background: var(--surface-2); }
|
|
.nav-link[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }
|
|
.nav-link svg { width: 20px; height: 20px; }
|
|
.nav-badge { position: static; margin-left: auto; }
|
|
|
|
.topbar { display: none; }
|
|
.view { padding-bottom: 0; overflow: auto; height: 100dvh; }
|
|
.view-page { padding: 8px 32px 32px; }
|
|
|
|
.view-queue {
|
|
display: grid; grid-template-columns: minmax(340px, 420px) 1fr;
|
|
overflow: hidden;
|
|
}
|
|
.view-queue .pane { overflow: auto; height: 100dvh; }
|
|
.pane-list { border-right: 1px solid var(--border); }
|
|
.pane-list .chips { position: sticky; top: 0; z-index: 2; background: var(--bg); padding-top: 16px; }
|
|
.view-queue:not(.has-detail) .pane-detail { display: block; }
|
|
|
|
/* On desktop the list stays visible next to the detail. */
|
|
.app.detail-open .nav { display: flex; }
|
|
.app.detail-open .pane-list { display: block; }
|
|
.detail-head .back { display: none; }
|
|
.detail-head { padding-left: 16px; }
|
|
|
|
.pane-detail { position: relative; display: flex; flex-direction: column; }
|
|
.detail { flex: 1; padding: 0 32px 24px; max-width: 900px; width: 100%; }
|
|
.detail-head { margin: 0 -32px; padding-left: 32px; }
|
|
.actionbar {
|
|
position: sticky; bottom: 0;
|
|
padding: 12px 32px;
|
|
}
|
|
.actionbar .btn-primary { flex: 0 1 240px; }
|
|
|
|
.sheet {
|
|
width: min(440px, calc(100% - 32px));
|
|
margin: auto; border-radius: 14px;
|
|
}
|
|
.sheet-grab { display: none; }
|
|
.sheet-inner { padding: 20px; }
|
|
|
|
.toast, .app.detail-open ~ .toast { bottom: 24px; }
|
|
.only-desktop { display: block; }
|
|
}
|
|
|
|
/* --- admin ---------------------------------------------------------------
|
|
The admin page is three tables of things you act on, so it needs table
|
|
styling the rest of the app never did: the queue is a list of links and the
|
|
account page is a form. */
|
|
.admin-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
.admin-table th {
|
|
text-align: left; font-weight: 600; color: var(--muted); font-size: 12px;
|
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
padding: 4px 8px 4px 0; border-bottom: 1px solid var(--border);
|
|
}
|
|
.admin-table td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
.admin-table tr:last-child td { border-bottom: none; }
|
|
.admin-table .num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
.admin-table td .btn-sm + .btn-sm { margin-left: 6px; }
|
|
/* A disabled account stays readable — it is still the name on old
|
|
acknowledgements — but should not look like a working one. */
|
|
.disabled-row td { opacity: 0.55; }
|
|
.btn-sm.danger { color: var(--crit); border-color: var(--crit-soft); }
|
|
|
|
.inline-form { display: flex; gap: 8px; margin-top: 12px; }
|
|
.inline-form input { flex: 1; min-width: 0; }
|
|
|
|
.admin-settings .setting-value { width: 5.5em; margin-right: 6px; }
|
|
.admin-settings .setting-unit { max-width: 8em; }
|
|
.admin-settings button[type="submit"] { margin-top: 12px; }
|
|
.small { font-size: 13px; }
|
|
|
|
/* --- team settings -------------------------------------------------------
|
|
Forms with a label above each control, rather than the queue's rows of
|
|
links. The escalation ladder is the only nested structure in the app, so it
|
|
gets a little indentation to make the levels read as an order. */
|
|
.stacked-form { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; align-items: flex-start; }
|
|
.stacked-form label { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 14px; }
|
|
.stacked-form label.checkbox { gap: 8px; }
|
|
.stacked-form input.wide { min-width: min(420px, 100%); }
|
|
.team-picker { margin-top: 8px; max-width: 100%; }
|
|
|
|
.ladder-level {
|
|
border-left: 3px solid var(--border-strong);
|
|
padding: 8px 0 8px 12px; margin: 12px 0;
|
|
}
|
|
.ladder-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
|
|
.ladder-targets { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
|
|
.target-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
|
|
/* An integration key is shown exactly once, so it should look like something
|
|
to act on rather than another row of text. */
|
|
.key-panel {
|
|
margin-top: 12px; padding: 12px;
|
|
border: 1px solid var(--accent); border-radius: 8px; background: var(--accent-soft);
|
|
}
|
|
.key-panel pre {
|
|
overflow-x: auto; background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: 6px; padding: 8px; font-size: 12px;
|
|
}
|
|
.key-url code { word-break: break-all; }
|