Two features stitched together because they touch the same building-
job data path.
--- 1. Retract preview DM on cancel -----------------------------------
The /jobs/edit flow DMs a preview of the composed message to the
sender's own Webex space. Until now that DM lingered even if the
sender then hit Cancel or Discard.
- /jobs/edit now stores the returned message id as
jobs.building[key].previewMessageId.
- /jobs/cancel captures that id before deleting the building entry,
saves the cancel first, then fires a best-effort
DELETE /v1/messages/<id> against Webex.
- New deleteWebexMessage(messageId, appName) helper wraps the DELETE.
Uses the bot token (bots own their messages) and never throws — a
Webex hiccup logs but doesn't fail the cancel that already
succeeded on our side. Called fire-and-forget so the HTTP response
isn't blocked on a slow Webex round trip.
--- 2. Admin: manage authorized users -------------------------------
Admin authority lives in a new top-level config.admins array of
personIds (seeded with Joe's id). Admin actions are cross-bot in
concept but the routes are :app-scoped because the resource being
edited is per-bot and it lets admin reuse the existing OAuth session
without a separate auth surface.
Helpers
- lib/helpers.js: new pure isAdmin(config, personId) that fails
closed when admins is missing, not-an-array, or config is null.
- test/helpers.test.js: 5 new assertions covering the happy path,
the "not in list" case, missing personId, non-array admins, and
missing config. Total suite is now 48 assertions across 11 groups.
Server-side (index.js)
- New findPersonByEmail(email) helper hits Webex /v1/people?email=
using the service account token, returns
{ id, displayName, email, avatar } or null.
- /info now returns isAdmin so the client can decide whether to
render the admin dropdown.
- New requireAdmin(req, res) gate returns 401 for signed-out and
403 for signed-in-but-not-admin (distinct codes so the frontend
can render distinct panels).
- GET /CollabCentral/:app/admin/users → list users
- POST /CollabCentral/:app/admin/users → lookup + add
- DELETE /CollabCentral/:app/admin/users/:id → remove
- Shared adminUserRow / adminUsersList shape so every response is an
authoritative snapshot the client can render without merging.
- DELETE of an unknown id is idempotent — returns 200 removed:false
without rewriting config.json.
Frontend
- New html/admin.html + html/admin.js on the shared layout. Panels
swap between not-signed-in / not-admin / admin. Add-user form
takes an email; user list renders as rows with Webex avatar
(fallback initials), name, email, favorite-group count, and a
Remove button that confirm()s before firing DELETE.
- html/js/app.js: renderUserChip() replaces the plain-text top-right
user label with a proper button + dropdown menu when the caller
is an admin. Menu is keyboard-friendly (Escape to close), closes
on outside-click, and currently exposes one item ("Admin" →
admin.html). Non-admins get the plain-text label unchanged, so
the existing pages are visually identical for them.
- html/css/app.css: new .appHeader__userBtn / .appHeader__userMenu
dropdown, .inlineFieldRow for the email-plus-button pattern, and
.userRow* rules for the admin user list.
Config
- Add config.admins array seeded with Joe McQueen's personId.
- config.json also picks up an in-app state change from the running
instance (an "AV Team" favorite removed from Joe's techupdates
authorized entry via the favorites UI). Rolling that into this
commit so the file stops drifting from origin.
Co-authored-by: Cursor <cursoragent@cursor.com>
779 lines
18 KiB
CSS
779 lines
18 KiB
CSS
/* ============================================================================
|
|
CollabCentral shared UI. All three top-level pages (sendMessage, monitorJobs,
|
|
jobDetail) render on top of this stylesheet — every rule below is meant to
|
|
be reusable across pages, and per-page tweaks stay in a small `<style>` at
|
|
the top of each HTML file.
|
|
============================================================================ */
|
|
|
|
:root {
|
|
--bg: #f8fafc;
|
|
--card: #ffffff;
|
|
--border: #e5e7eb;
|
|
--border-strong: #cbd5e1;
|
|
--text: #0f172a;
|
|
--text-muted: #64748b;
|
|
--text-subtle: #94a3b8;
|
|
--accent: #2563eb;
|
|
--accent-hover: #1d4ed8;
|
|
--accent-soft: #dbeafe;
|
|
--success: #15803d;
|
|
--success-bg: #dcfce7;
|
|
--danger: #b91c1c;
|
|
--danger-bg: #fee2e2;
|
|
--warning: #a16207;
|
|
--warning-bg: #fef9c3;
|
|
--radius: 8px;
|
|
--radius-sm: 4px;
|
|
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05);
|
|
--shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
|
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
--content-max: 1100px;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font);
|
|
font-size: 15px;
|
|
line-height: 1.5;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
a {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
}
|
|
a:hover { color: var(--accent-hover); text-decoration: underline; }
|
|
|
|
.hidden { display: none !important; }
|
|
|
|
/* ------- Top nav ---------------------------------------------------------- */
|
|
|
|
.appHeader {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
background: var(--card);
|
|
border-bottom: 1px solid var(--border);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.appHeader__inner {
|
|
max-width: var(--content-max);
|
|
margin: 0 auto;
|
|
padding: 0.6em 1.25em;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1em;
|
|
}
|
|
|
|
.appHeader__brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.7em;
|
|
min-width: 0;
|
|
}
|
|
|
|
.appHeader__avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: var(--border);
|
|
background-size: cover;
|
|
background-position: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.appHeader__label {
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
color: var(--text);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.appHeader__nav {
|
|
display: flex;
|
|
gap: 0.4em;
|
|
margin-left: 1em;
|
|
}
|
|
|
|
.appHeader__nav a {
|
|
display: inline-block;
|
|
padding: 0.4em 0.85em;
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
}
|
|
.appHeader__nav a:hover {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
}
|
|
.appHeader__nav a[aria-current="page"] {
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.appHeader__spacer { flex: 1; }
|
|
|
|
.appHeader__user {
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 20ch;
|
|
}
|
|
|
|
/* When the user chip is a menu (admin case), it becomes a positioned
|
|
dropdown container. Non-menu chips keep the plain-text ellipsis look. */
|
|
.appHeader__user--menu {
|
|
position: relative;
|
|
overflow: visible;
|
|
max-width: none;
|
|
}
|
|
|
|
.appHeader__userBtn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.35em;
|
|
padding: 0.3em 0.6em;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-muted);
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
max-width: 20ch;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
}
|
|
.appHeader__userBtn:hover,
|
|
.appHeader__userBtn[aria-expanded="true"] {
|
|
background: var(--bg);
|
|
border-color: var(--border-strong);
|
|
color: var(--text);
|
|
}
|
|
.appHeader__userChevron {
|
|
font-size: 0.75em;
|
|
line-height: 1;
|
|
color: inherit;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.appHeader__userMenu {
|
|
position: absolute;
|
|
top: calc(100% + 0.35em);
|
|
right: 0;
|
|
z-index: 30;
|
|
min-width: 12em;
|
|
margin: 0;
|
|
padding: 0.35em 0;
|
|
list-style: none;
|
|
background: var(--card);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
|
}
|
|
.appHeader__userMenu li { margin: 0; }
|
|
.appHeader__userMenu a {
|
|
display: block;
|
|
padding: 0.5em 0.9em;
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
}
|
|
.appHeader__userMenu a:hover,
|
|
.appHeader__userMenu a:focus-visible {
|
|
background: var(--bg);
|
|
outline: none;
|
|
}
|
|
|
|
/* ------- Page container / cards ------------------------------------------ */
|
|
|
|
.container {
|
|
max-width: var(--content-max);
|
|
margin: 1.5em auto;
|
|
padding: 0 1.25em;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-sm);
|
|
padding: 1.5em;
|
|
margin-bottom: 1.5em;
|
|
}
|
|
|
|
.card h3 { margin-top: 0; }
|
|
|
|
.pageTitle {
|
|
margin: 0 0 0.2em;
|
|
font-size: 1.35rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pageSubtitle {
|
|
color: var(--text-muted);
|
|
margin: 0 0 1.25em;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* ------- Panels (empty state) -------------------------------------------- */
|
|
|
|
.panel {
|
|
max-width: 560px;
|
|
margin: 4em auto;
|
|
padding: 2.5em 2em;
|
|
text-align: center;
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.panel h2 {
|
|
margin: 0 0 0.4em;
|
|
font-size: 1.25rem;
|
|
color: var(--text);
|
|
}
|
|
.panel p {
|
|
margin: 0;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* ------- Forms ----------------------------------------------------------- */
|
|
|
|
.field {
|
|
margin-bottom: 1.1em;
|
|
}
|
|
|
|
.field > label {
|
|
display: block;
|
|
margin-bottom: 0.35em;
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.field input[type="text"],
|
|
.field input[type="datetime-local"],
|
|
.field input[type="file"],
|
|
.field textarea,
|
|
.field select {
|
|
width: 100%;
|
|
padding: 0.55em 0.75em;
|
|
background: var(--card);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
font: inherit;
|
|
color: var(--text);
|
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
}
|
|
|
|
.field input[type="text"]:focus,
|
|
.field input[type="datetime-local"]:focus,
|
|
.field textarea:focus,
|
|
.field select:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
|
}
|
|
|
|
.field .hint {
|
|
display: block;
|
|
margin-top: 0.3em;
|
|
color: var(--text-muted);
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
/* Label row: puts the label and an inline transient status message on the
|
|
same line without disturbing the vertical rhythm of surrounding fields. */
|
|
.fieldLabelRow {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 0.75em;
|
|
margin-bottom: 0.35em;
|
|
}
|
|
|
|
.fieldLabelRow > label {
|
|
margin-bottom: 0;
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.fieldStatus {
|
|
font-size: 0.8rem;
|
|
color: var(--accent, #2563eb);
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
min-height: 1em;
|
|
}
|
|
|
|
.fieldStatus:not(:empty) {
|
|
opacity: 1;
|
|
}
|
|
|
|
.fieldStatus--error {
|
|
color: #b91c1c;
|
|
}
|
|
|
|
/* Chip strip: compact list of removable pills used by the "Manage
|
|
favorites" affordance below the favorite-groups picker. Chips are
|
|
click-target friendly on touch (min 32px tall) and truncate long
|
|
labels with ellipsis so a wall of group names doesn't blow out the
|
|
card width. */
|
|
.chipStrip {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4em;
|
|
margin-top: 0.55em;
|
|
}
|
|
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.35em;
|
|
max-width: 100%;
|
|
padding: 0.3em 0.35em 0.3em 0.7em;
|
|
background: var(--surface-muted, #f1f5f9);
|
|
border: 1px solid var(--border-strong, #cbd5e1);
|
|
border-radius: 999px;
|
|
font-size: 0.82rem;
|
|
line-height: 1.3;
|
|
color: var(--text, #0f172a);
|
|
}
|
|
|
|
.chip__label {
|
|
max-width: 22ch;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.chip__close {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 1.35em;
|
|
height: 1.35em;
|
|
padding: 0;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-muted, #64748b);
|
|
font-size: 1.05rem;
|
|
line-height: 1;
|
|
border-radius: 999px;
|
|
cursor: pointer;
|
|
transition: background 0.15s, color 0.15s;
|
|
}
|
|
|
|
.chip__close:hover:not(:disabled),
|
|
.chip__close:focus-visible {
|
|
background: #fee2e2;
|
|
color: #b91c1c;
|
|
outline: none;
|
|
}
|
|
|
|
.chip__close:disabled {
|
|
opacity: 0.5;
|
|
cursor: default;
|
|
}
|
|
|
|
.field-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1em;
|
|
}
|
|
|
|
/* File input tweak: browsers style these very differently; give it some room */
|
|
.field input[type="file"] {
|
|
padding: 0.4em 0.6em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* EasyMDE overrides so it fits our neutral theme */
|
|
.EasyMDEContainer .CodeMirror {
|
|
border-color: var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
background: var(--card);
|
|
}
|
|
.EasyMDEContainer .editor-toolbar {
|
|
border-color: var(--border-strong);
|
|
background: var(--bg);
|
|
border-top-left-radius: var(--radius-sm);
|
|
border-top-right-radius: var(--radius-sm);
|
|
}
|
|
.EasyMDEContainer .editor-preview,
|
|
.EasyMDEContainer .editor-preview-side {
|
|
background: var(--card);
|
|
color: var(--text);
|
|
}
|
|
|
|
/* Image preview */
|
|
#file-ip-1-preview {
|
|
display: block;
|
|
max-width: 100%;
|
|
max-height: 260px;
|
|
border-radius: var(--radius-sm);
|
|
margin-top: 0.4em;
|
|
object-fit: contain;
|
|
}
|
|
#file-ip-1-preview:not([src]) { display: none; }
|
|
|
|
/* ------- Buttons --------------------------------------------------------- */
|
|
|
|
.btn {
|
|
display: inline-block;
|
|
padding: 0.55em 1.1em;
|
|
border-radius: var(--radius-sm);
|
|
border: 1px solid transparent;
|
|
background: var(--card);
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
}
|
|
.btn:hover { background: var(--bg); }
|
|
.btn:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--accent);
|
|
color: white;
|
|
border-color: var(--accent);
|
|
}
|
|
.btn-primary:hover {
|
|
background: var(--accent-hover);
|
|
border-color: var(--accent-hover);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--card);
|
|
color: var(--text);
|
|
border-color: var(--border-strong);
|
|
}
|
|
.btn-secondary:hover {
|
|
background: var(--bg);
|
|
border-color: var(--text-muted);
|
|
}
|
|
|
|
/* Destructive action (discard draft, delete favorite, etc). Muted so it
|
|
doesn't overpower the primary CTA — the color is the signal. */
|
|
.btn-danger {
|
|
background: transparent;
|
|
color: #b91c1c;
|
|
border-color: #fecaca;
|
|
}
|
|
.btn-danger:hover {
|
|
background: #fef2f2;
|
|
border-color: #f87171;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.btn-ghost {
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
border-color: transparent;
|
|
padding: 0.35em 0.7em;
|
|
}
|
|
.btn-ghost:hover {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
}
|
|
|
|
.formActions {
|
|
display: flex;
|
|
gap: 0.6em;
|
|
align-items: center;
|
|
margin-top: 1em;
|
|
}
|
|
|
|
/* ------- Tables (DataTables-friendly) ------------------------------------ */
|
|
|
|
table.dataTable {
|
|
width: 100% !important;
|
|
border-collapse: collapse;
|
|
font-size: 0.92rem;
|
|
}
|
|
table.dataTable thead th {
|
|
background: var(--bg);
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
font-size: 0.82rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
border-bottom: 1px solid var(--border) !important;
|
|
padding: 0.7em 0.8em !important;
|
|
text-align: left;
|
|
}
|
|
table.dataTable tbody td {
|
|
padding: 0.7em 0.8em !important;
|
|
border-bottom: 1px solid var(--border);
|
|
color: var(--text);
|
|
vertical-align: top;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
table.dataTable tbody tr:hover {
|
|
background: var(--bg);
|
|
}
|
|
table.dataTable tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
table.dataTable.no-footer { border-bottom: none; }
|
|
|
|
.dataTables_wrapper .dataTables_filter input {
|
|
padding: 0.35em 0.6em;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
font: inherit;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_length select {
|
|
padding: 0.3em 0.5em;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
font: inherit;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_info,
|
|
.dataTables_wrapper .dataTables_paginate {
|
|
color: var(--text-muted);
|
|
font-size: 0.85rem;
|
|
padding-top: 0.75em;
|
|
}
|
|
.dataTables_wrapper .paginate_button {
|
|
padding: 0.35em 0.7em !important;
|
|
margin-left: 0.15em;
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-muted) !important;
|
|
border: 1px solid transparent !important;
|
|
}
|
|
.dataTables_wrapper .paginate_button.current,
|
|
.dataTables_wrapper .paginate_button:hover {
|
|
background: var(--accent-soft) !important;
|
|
color: var(--accent) !important;
|
|
border-color: var(--accent-soft) !important;
|
|
}
|
|
|
|
/* ------- Job-detail summary grid ----------------------------------------- */
|
|
|
|
.summary-grid {
|
|
display: grid;
|
|
grid-template-columns: max-content 1fr;
|
|
gap: 0.5em 1.5em;
|
|
padding: 1.1em 1.25em;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
margin: 0 0 1.5em;
|
|
}
|
|
.summary-grid dt {
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
font-size: 0.88rem;
|
|
}
|
|
.summary-grid dd {
|
|
margin: 0;
|
|
color: var(--text);
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
margin: 1.5em 0 0.5em;
|
|
}
|
|
|
|
.message-preview {
|
|
padding: 1em 1.25em;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
.message-preview img { max-width: 100%; height: auto; }
|
|
.message-preview p:first-child { margin-top: 0; }
|
|
.message-preview p:last-child { margin-bottom: 0; }
|
|
|
|
/* ------- Pills ----------------------------------------------------------- */
|
|
|
|
.pill {
|
|
display: inline-block;
|
|
padding: 0.15em 0.7em;
|
|
border-radius: 999px;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.pill.ok { background: var(--success-bg); color: var(--success); }
|
|
.pill.err { background: var(--danger-bg); color: var(--danger); }
|
|
.pill.pending { background: var(--warning-bg); color: var(--warning); }
|
|
|
|
/* ------- Modal ----------------------------------------------------------- */
|
|
|
|
.overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(15, 23, 42, 0.4);
|
|
backdrop-filter: blur(3px);
|
|
z-index: 20;
|
|
}
|
|
|
|
.modal {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 21;
|
|
width: min(440px, 92vw);
|
|
background: var(--card);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
padding: 1.5em 1.75em;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6em;
|
|
}
|
|
.modal h3 { margin: 0; }
|
|
.modal p { color: var(--text-muted); margin: 0; }
|
|
.modal__close {
|
|
align-self: flex-end;
|
|
background: transparent;
|
|
border: none;
|
|
font-size: 1.2rem;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
padding: 0.2em 0.4em;
|
|
border-radius: 999px;
|
|
}
|
|
.modal__close:hover { background: var(--bg); color: var(--text); }
|
|
.modal__actions {
|
|
display: flex;
|
|
gap: 0.6em;
|
|
justify-content: flex-end;
|
|
margin-top: 0.6em;
|
|
}
|
|
|
|
/* Push destructive actions inside the modal footer to the far left so the
|
|
affirmative "Send" / "Keep editing" pair stays on the right and the user
|
|
can't fat-finger Discard on their way to Send. */
|
|
.modal__actions .btn-danger {
|
|
margin-right: auto;
|
|
}
|
|
|
|
/* ------- Admin page: user list + add-user form -------------------------- */
|
|
|
|
/* One-row form with input + button laid out side-by-side. Wraps on
|
|
narrow screens rather than overflowing the card. */
|
|
.inlineFieldRow {
|
|
display: flex;
|
|
gap: 0.6em;
|
|
align-items: stretch;
|
|
flex-wrap: wrap;
|
|
}
|
|
.inlineFieldRow input[type="email"],
|
|
.inlineFieldRow input[type="text"] {
|
|
flex: 1 1 20ch;
|
|
min-width: 12ch;
|
|
}
|
|
.inlineFieldRow .btn {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.userList {
|
|
margin-top: 1.2em;
|
|
}
|
|
.userList__items {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5em;
|
|
}
|
|
.userRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.9em;
|
|
padding: 0.6em 0.75em;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--card);
|
|
}
|
|
.userRow__avatar {
|
|
flex: 0 0 auto;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: var(--bg) center / cover no-repeat;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
}
|
|
.userRow__avatar--initials {
|
|
background: var(--surface-muted, #f1f5f9);
|
|
color: var(--text);
|
|
}
|
|
.userRow__meta {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
.userRow__name {
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.userRow__email {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.userRow__stats {
|
|
font-size: 0.78rem;
|
|
color: var(--text-muted);
|
|
margin-top: 0.15em;
|
|
}
|
|
.userRow__actions {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
/* ------- Responsive ------------------------------------------------------ */
|
|
|
|
@media (max-width: 640px) {
|
|
.field-row { grid-template-columns: 1fr; }
|
|
.appHeader__inner { flex-wrap: wrap; }
|
|
.appHeader__nav { margin-left: 0; }
|
|
.container { padding: 0 1em; }
|
|
.card { padding: 1.1em; }
|
|
}
|