The menu bar's show/hide tab was centred on the screen, exactly where a centred tile's own title-bar tab lands once both bars are hidden: two identical tabs on top of each other, with the tile's unreachable. The tab now sits at the left under the ProdDash brand and wears the accent colour, so it never coincides with a tile tab and reads unmistakably as the dashboard's. To keep it off a top-row tile's status dot and title it straddles the bar's bottom edge while the bar is shown (mostly on the bar, a short lip below) and becomes a slim tab at the very top edge when the bar is hidden. The Admin link is hidden in fullscreen, including when the menu bar is brought back there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
655 lines
16 KiB
CSS
655 lines
16 KiB
CSS
/* ProdDash shell — dark, high-contrast, production-booth friendly.
|
|
Visual language carried over from ProdCom Listener (same variable names, so
|
|
module CSS written against these looks native everywhere). Modules must
|
|
style with these variables only — no hardcoded colors. */
|
|
|
|
:root {
|
|
--bg: #0b0e12;
|
|
--panel: #131920;
|
|
--border: #232b35;
|
|
--text: #e6edf3;
|
|
--muted: #8b98a5;
|
|
--accent: #2ee59a;
|
|
--danger: #f4433c;
|
|
--warn: #f0b429;
|
|
/* grid geometry (shell.js reads these) */
|
|
--cell-h: 72px;
|
|
--gap: 0px; /* tiles sit flush; their 1px borders are the only seam */
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
html, body { height: 100%; }
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
button { font-family: inherit; }
|
|
|
|
/* ── topbar ────────────────────────────────────────────────────────── */
|
|
|
|
#topbar {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
height: 46px;
|
|
padding: 0 12px;
|
|
background: var(--panel);
|
|
border-bottom: 1px solid var(--border);
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
transition: height 0.22s ease, border-bottom-width 0.22s ease;
|
|
}
|
|
/* collapsed by its notch (and automatically in fullscreen) */
|
|
body.menu-hidden #topbar {
|
|
height: 0;
|
|
border-bottom-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* The menubar's own show/hide tab. It sits under the ProdDash brand at the
|
|
left — not screen-centre, where a tile's own (centred) tab would land
|
|
right under it once both bars are hidden — and wears the accent colour
|
|
so it never reads as a tile tab. */
|
|
#topbar-notch {
|
|
position: fixed;
|
|
/* straddles the bar's bottom edge — mostly on the bar, a short lip below
|
|
it — so it never covers a top-row tile's status dot or title */
|
|
top: calc(var(--topbar-h, 46px) - 8px);
|
|
left: 12px;
|
|
width: 54px;
|
|
height: 14px;
|
|
padding: 0;
|
|
background: var(--accent);
|
|
border: none;
|
|
border-radius: 0 0 10px 10px;
|
|
cursor: pointer;
|
|
z-index: 60;
|
|
opacity: 0.55;
|
|
transition: opacity 0.15s ease, top 0.22s ease, height 0.22s ease;
|
|
}
|
|
#topbar-notch:hover { opacity: 1; }
|
|
/* bar hidden: a slim tab at the very top edge, clear of a top-row tile's dot and title */
|
|
body.menu-hidden #topbar-notch { top: 0; height: 10px; }
|
|
#topbar-notch::after {
|
|
content: "";
|
|
display: block;
|
|
width: 20px;
|
|
height: 3px;
|
|
margin: 4px auto 0;
|
|
border-radius: 999px;
|
|
background: var(--bg);
|
|
}
|
|
|
|
#brand {
|
|
font-weight: 800;
|
|
font-size: 15px;
|
|
letter-spacing: 0.02em;
|
|
margin-right: 6px;
|
|
}
|
|
#brand .accent { color: var(--accent); }
|
|
|
|
#topbar .spacer { flex: 1; }
|
|
|
|
.tb-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 6px 10px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.tb-btn:hover { border-color: var(--muted); }
|
|
.tb-btn:active { background: var(--border); }
|
|
.tb-btn.icon {
|
|
padding: 6px 8px;
|
|
min-width: 30px;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
}
|
|
.tb-btn.icon svg { display: block; }
|
|
|
|
/* ── dropdown menus ────────────────────────────────────────────────── */
|
|
|
|
.dropdown { position: relative; }
|
|
|
|
.menu {
|
|
position: absolute;
|
|
top: calc(100% + 6px);
|
|
right: 0;
|
|
z-index: 50;
|
|
min-width: 210px;
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 5px;
|
|
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
|
|
}
|
|
.menu[hidden] { display: none; }
|
|
|
|
.menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
padding: 8px 10px;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
.menu-item:hover { background: var(--border); }
|
|
.menu-item .sub {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
margin-top: 2px;
|
|
white-space: normal;
|
|
}
|
|
.menu-item.danger { color: var(--danger); }
|
|
.menu-note {
|
|
padding: 8px 10px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
white-space: normal;
|
|
}
|
|
.menu-divider { height: 1px; background: var(--border); margin: 5px 4px; }
|
|
.menu-title {
|
|
padding: 6px 10px 3px;
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* ── grid ──────────────────────────────────────────────────────────── */
|
|
|
|
#gridwrap {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
#grid {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
grid-auto-rows: var(--cell-h);
|
|
gap: var(--gap);
|
|
/* Always leave room to drop/grow a tile below the last row. */
|
|
padding-bottom: calc(3 * (var(--cell-h) + var(--gap)));
|
|
}
|
|
|
|
#empty-state {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding-top: 12vh;
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
pointer-events: none;
|
|
}
|
|
#empty-state[hidden] { display: none; }
|
|
|
|
/* ── tiles ─────────────────────────────────────────────────────────── */
|
|
|
|
.tile {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tile.dragging,
|
|
.tile.resizing {
|
|
z-index: 40;
|
|
border-color: var(--muted);
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
|
|
opacity: 0.92;
|
|
}
|
|
|
|
.tile-head {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
height: 32px; /* fixed so the notch below can sit at a known offset */
|
|
padding: 0 8px;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border-bottom: 1px solid var(--border);
|
|
cursor: grab;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
touch-action: none;
|
|
transition: height 0.22s ease, border-bottom-width 0.22s ease;
|
|
}
|
|
.tile.dragging .tile-head { cursor: grabbing; }
|
|
/* hidden = collapsed, not removed, so the notch toggle animates */
|
|
.tile.head-hidden .tile-head {
|
|
height: 0;
|
|
border-bottom-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tile-head .dot {
|
|
flex: none;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
transition: background 0.3s;
|
|
}
|
|
.tile-head .dot.ok { background: var(--accent); box-shadow: 0 0 5px var(--accent); }
|
|
.tile-head .dot.connecting { background: var(--warn); }
|
|
.tile-head .dot.error { background: var(--danger); }
|
|
|
|
.tile-title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
letter-spacing: 0.03em;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.tile-btn {
|
|
flex: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 22px;
|
|
height: 22px;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 5px;
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
.tile-btn:hover { background: var(--border); color: var(--text); }
|
|
.tile-btn.close:hover { color: var(--danger); }
|
|
.tile-btn.active { color: var(--accent); }
|
|
.tile-btn svg { width: 13px; height: 13px; display: block; }
|
|
|
|
/* controls a module adds to its own title bar (moduleApi.header) */
|
|
.tile-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
.tile-btn.module-btn {
|
|
width: auto;
|
|
min-width: 22px;
|
|
padding: 0 5px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
.tile-dd { position: relative; display: inline-flex; }
|
|
|
|
/* a module header menu — fixed-positioned by the shell so the tile's
|
|
overflow:hidden can't clip it */
|
|
.tile-menu {
|
|
position: fixed;
|
|
z-index: 70;
|
|
min-width: 180px;
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 5px;
|
|
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
|
|
}
|
|
.tile-menu[hidden] { display: none; }
|
|
|
|
/* notch on the title bar's bottom edge: click to hide/show the bar
|
|
(same idea as the ProdCom Listener's pull-down menubar handle) */
|
|
.tile-notch {
|
|
position: absolute;
|
|
top: 32px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 46px;
|
|
height: 13px;
|
|
padding: 0;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
border-radius: 0 0 10px 10px;
|
|
cursor: pointer;
|
|
z-index: 6;
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease, top 0.22s ease;
|
|
}
|
|
.tile:hover .tile-notch { opacity: 0.6; }
|
|
.tile .tile-notch:hover { opacity: 1; }
|
|
.tile.head-hidden .tile-notch { top: 0; }
|
|
.tile-notch::after {
|
|
content: "";
|
|
display: block;
|
|
width: 18px;
|
|
height: 3px;
|
|
margin: 4px auto 0;
|
|
border-radius: 999px;
|
|
background: var(--muted);
|
|
}
|
|
.tile-notch:hover::after { background: var(--text); }
|
|
|
|
.tile-body {
|
|
flex: 1;
|
|
position: relative;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tile-resize,
|
|
.tile-resize-left {
|
|
position: absolute;
|
|
bottom: 0;
|
|
width: 20px;
|
|
height: 20px;
|
|
touch-action: none;
|
|
z-index: 5;
|
|
color: var(--muted);
|
|
opacity: 0; /* corner indicators only show while hovering the tile */
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
.tile-resize { right: 0; cursor: nwse-resize; }
|
|
.tile-resize-left { left: 0; cursor: nesw-resize; }
|
|
.tile:hover .tile-resize,
|
|
.tile:hover .tile-resize-left { opacity: 0.6; }
|
|
.tile .tile-resize:hover,
|
|
.tile .tile-resize-left:hover { opacity: 1; color: var(--text); }
|
|
.tile-resize::after,
|
|
.tile-resize-left::after {
|
|
content: "";
|
|
position: absolute;
|
|
bottom: 4px;
|
|
width: 9px;
|
|
height: 9px;
|
|
border-bottom: 2px solid currentColor;
|
|
}
|
|
.tile-resize::after {
|
|
right: 4px;
|
|
border-right: 2px solid currentColor;
|
|
border-radius: 0 0 3px 0;
|
|
}
|
|
.tile-resize-left::after {
|
|
left: 4px;
|
|
border-left: 2px solid currentColor;
|
|
border-radius: 0 0 0 3px;
|
|
}
|
|
|
|
/* bottom-edge pill: drag to move the tile's bottom edge (height only);
|
|
sits between the two corner handles */
|
|
.tile-resize-bottom {
|
|
position: absolute;
|
|
left: 24px;
|
|
right: 24px;
|
|
bottom: 0;
|
|
height: 10px;
|
|
z-index: 5;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
cursor: ns-resize;
|
|
touch-action: none;
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
.tile-resize-bottom::after {
|
|
content: "";
|
|
width: 44px;
|
|
max-width: 60%;
|
|
height: 5px;
|
|
margin-bottom: 2px;
|
|
border-radius: 999px;
|
|
background: var(--muted);
|
|
transition: background 0.15s ease;
|
|
}
|
|
.tile:hover .tile-resize-bottom { opacity: 0.6; }
|
|
.tile .tile-resize-bottom:hover,
|
|
.tile.resizing .tile-resize-bottom { opacity: 1; }
|
|
.tile .tile-resize-bottom:hover::after,
|
|
.tile.resizing .tile-resize-bottom::after { background: var(--text); }
|
|
|
|
/* ── seam divider between adjacent tiles ───────────────────────────── */
|
|
|
|
/* When two tiles share a vertical edge, an invisible strip sits on the seam;
|
|
hovering reveals a small macOS-split-view-style grab pill. Dragging it
|
|
trades width between the two tiles. */
|
|
.tile-divider {
|
|
position: absolute;
|
|
width: 14px;
|
|
transform: translateX(-50%);
|
|
z-index: 30;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: col-resize;
|
|
touch-action: none;
|
|
}
|
|
|
|
.tile-divider::after {
|
|
content: "";
|
|
width: 5px;
|
|
height: 44px;
|
|
max-height: 55%;
|
|
border-radius: 999px;
|
|
background: var(--muted);
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease, background 0.15s ease;
|
|
}
|
|
|
|
.tile-divider:hover::after { opacity: 0.7; }
|
|
.tile-divider.active::after { opacity: 1; background: var(--text); }
|
|
|
|
/* Hide the pills while a tile itself is being moved or corner-resized —
|
|
they're rebuilt for the new geometry on release. */
|
|
#grid.no-dividers .tile-divider { display: none; }
|
|
|
|
/* ── fullscreen mode ───────────────────────────────────────────────── */
|
|
|
|
/* Fullscreen is a viewing mode: the layout auto-fits the screen, so the
|
|
editing affordances (corner handles, seam pills) are parked. */
|
|
body.fullscreen .tile-resize,
|
|
body.fullscreen .tile-resize-left,
|
|
body.fullscreen .tile-resize-bottom,
|
|
body.fullscreen .tile-divider,
|
|
body.fullscreen #admin-link { display: none; }
|
|
body.fullscreen #grid { padding-bottom: 0; }
|
|
#gridwrap.no-scroll { overflow-y: hidden; }
|
|
|
|
/* status message a module can show while degraded (also used by the shell
|
|
when a module fails to load) */
|
|
.tile-msg {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
padding: 12px;
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
background: var(--panel);
|
|
z-index: 4;
|
|
}
|
|
|
|
/* ── per-tile settings popover ─────────────────────────────────────── */
|
|
|
|
.tile-settings {
|
|
position: absolute;
|
|
top: 36px;
|
|
right: 6px;
|
|
z-index: 45;
|
|
min-width: 220px;
|
|
max-width: calc(100% - 12px);
|
|
max-height: calc(100% - 40px);
|
|
overflow-y: auto;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin-bottom: 10px;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
}
|
|
.field input[type="text"],
|
|
.field input[type="number"],
|
|
.field input[type="password"],
|
|
.field select {
|
|
padding: 7px 9px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 7px;
|
|
}
|
|
.field input:focus, .field select:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
.field.check {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
.field.check input { width: 15px; height: 15px; accent-color: var(--accent); }
|
|
|
|
/* ── modal dialogs (layout name prompt etc.) ───────────────────────── */
|
|
|
|
#modal-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 90;
|
|
background: rgba(4, 6, 8, 0.65);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding-top: 18vh;
|
|
}
|
|
#modal-backdrop[hidden] { display: none; }
|
|
|
|
#modal {
|
|
width: min(360px, calc(100vw - 32px));
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
|
|
}
|
|
#modal h2 { font-size: 14px; margin-bottom: 12px; }
|
|
#modal .actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.btn {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 6px 12px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
.btn:hover { border-color: var(--muted); }
|
|
.btn.primary {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: #06281a;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* toast */
|
|
#toast {
|
|
position: fixed;
|
|
bottom: 18px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 95;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
border-radius: 999px;
|
|
padding: 8px 16px;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
transition: opacity 0.25s ease;
|
|
}
|
|
#toast[hidden] { display: none; }
|
|
#toast.error { border-color: var(--danger); }
|
|
|
|
/* ── scrollbars ────────────────────────────────────────────────────── */
|
|
|
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--border);
|
|
border-radius: 999px;
|
|
border: 2px solid transparent; /* inset gutter */
|
|
background-clip: padding-box;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #364253;
|
|
background-clip: padding-box;
|
|
}
|
|
::-webkit-scrollbar-corner { background: transparent; }
|
|
|
|
/* Firefox (webkit pseudo-elements unsupported there) */
|
|
@supports (-moz-appearance: none) {
|
|
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
#topbar { padding: 0 8px; gap: 6px; }
|
|
}
|