No title and no ✕ — the window sits beside its tile and closes on a click anywhere else or Escape — and it is as wide as its fields want, so a couple of switches make a small window. Help lines wrap to the fields' width instead of widening it. Guide: one control per action — whatever has a header button is not offered again in the gear. ProdCom 1.2.2 drops its timestamps and newest-first gear switches (both are header buttons). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
946 lines
26 KiB
CSS
946 lines
26 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;
|
|
--accent-text: #06281a; /* text that sits on --accent (buttons, badges) */
|
|
--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 bar's show/hide handle: a small accent lip on its bottom edge, the
|
|
same idea as a tile's notch. It sits at the left — never screen-centre,
|
|
where a tile's own (centred) tab lands once both bars are hidden — and
|
|
grows under the pointer so it is easy to hit without being loud at rest.
|
|
It used to be the ProdDash wordmark on a pill hanging past the bar, which
|
|
read as a step cut out of the bar, and animating its font-size on the way
|
|
out was the jerkiest thing on the page in Safari. Everything here moves by
|
|
transform, opacity and its own fixed size: no text metrics, no reflow. */
|
|
#topbar-notch {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 10px;
|
|
width: 28px;
|
|
height: 5px;
|
|
padding: 0;
|
|
border: none;
|
|
border-radius: 0 0 5px 5px;
|
|
background: var(--accent);
|
|
opacity: 0.5;
|
|
cursor: pointer;
|
|
z-index: 60;
|
|
/* Glued to the bar's bottom edge: --topbar-h is the bar's live height, so
|
|
the lip rides the bar's own collapse instead of running a second,
|
|
competing animation against it. */
|
|
transform: translateY(var(--topbar-h, 46px));
|
|
transition: width 0.16s ease, height 0.16s ease,
|
|
opacity 0.16s ease, border-radius 0.16s ease;
|
|
}
|
|
#topbar-notch:hover,
|
|
#topbar-notch:focus-visible {
|
|
width: 60px;
|
|
height: 10px;
|
|
opacity: 1;
|
|
border-radius: 0 0 8px 8px;
|
|
}
|
|
/* With the bar collapsed --topbar-h is 0, so the lip ends up at the very top
|
|
edge without a rule of its own. */
|
|
|
|
#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[disabled] { opacity: 0.45; cursor: default; }
|
|
.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); }
|
|
|
|
/* Fullscreen is for watching, not arranging: every title bar folds away, and
|
|
a tile's own hidden/shown preference is left untouched for when the screen
|
|
comes back. The notch still peeks one open, and it folds back once the
|
|
pointer has been off that tile for a while (PEEK_HIDE_MS in shell.js). */
|
|
body.fullscreen .tile:not(.head-peek) .tile-head {
|
|
height: 0;
|
|
border-bottom-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
body.fullscreen .tile:not(.head-peek) .tile-notch { top: 0; }
|
|
body.fullscreen .tile.head-peek .tile-head {
|
|
height: 32px;
|
|
border-bottom-width: 1px;
|
|
}
|
|
body.fullscreen .tile.head-peek .tile-notch { top: 32px; }
|
|
|
|
.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: drag to move the tile's bottom edge (height only). Drawn as
|
|
the same 2px line as the corner handles it sits between, on the same
|
|
4px inset, so the three read as one set of marks along the bottom. */
|
|
.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;
|
|
color: var(--muted);
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
.tile-resize-bottom::after {
|
|
content: "";
|
|
width: 44px;
|
|
max-width: 60%;
|
|
height: 0;
|
|
margin-bottom: 4px;
|
|
border-bottom: 2px solid currentColor;
|
|
}
|
|
.tile:hover .tile-resize-bottom { opacity: 0.6; }
|
|
.tile .tile-resize-bottom:hover,
|
|
.tile.resizing .tile-resize-bottom { opacity: 1; color: 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,
|
|
.tile-divider.linked::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 ─────────────────────────────────────── */
|
|
|
|
/* A small window beside its tile (shell.js places and clamps it), not a
|
|
box inside the tile: a tile can be smaller than its own settings, and the
|
|
whole point of live settings is watching the tile change. */
|
|
.tile-settings {
|
|
position: fixed;
|
|
z-index: 80;
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* as wide as its content wants and no wider: a couple of switches make a
|
|
small window, a three-column checklist a wide one */
|
|
width: max-content;
|
|
min-width: 150px;
|
|
max-width: min(560px, calc(100vw - 16px));
|
|
max-height: calc(100vh - 16px);
|
|
overflow: hidden;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55);
|
|
}
|
|
/* the grab bar: a slim strip with a grip mark, nothing to read or press */
|
|
.tile-settings-bar {
|
|
flex: none;
|
|
height: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border-bottom: 1px solid var(--border);
|
|
cursor: grab;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
touch-action: none;
|
|
}
|
|
.tile-settings-bar::before {
|
|
content: "";
|
|
width: 34px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: var(--muted);
|
|
opacity: 0.55;
|
|
transition: opacity 0.12s;
|
|
}
|
|
.tile-settings-bar:hover::before { opacity: 0.9; }
|
|
.tile-settings-bar:active { cursor: grabbing; }
|
|
.tile-settings-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
}
|
|
/* Help lines never widen the window: they take no room of their own when the
|
|
content is measured, then wrap to whatever width the fields settled on. */
|
|
.tile-settings .field-help,
|
|
.tile-settings .field-group-help,
|
|
.tile-settings .multiselect-empty { width: 0; min-width: 100%; }
|
|
/* a group's on/off sits in its title row; the body dims while it is off */
|
|
.tile-settings .field-group-title { display: flex; align-items: center; gap: 8px; }
|
|
.tile-settings .field-group-name { flex: 1; min-width: 0; }
|
|
.tile-settings .group-toggle { margin: 0 0 0 auto; }
|
|
.tile-settings .group-toggle .check-label { display: none; }
|
|
.tile-settings .field-group.is-off .field-group-body { opacity: 0.45; }
|
|
/* several picks from a list (type `multiselect`) */
|
|
.tile-settings .multiselect-field { display: flex; flex-direction: column; gap: 4px; }
|
|
.tile-settings .multiselect { display: flex; flex-direction: column; gap: 3px; }
|
|
.tile-settings .multiselect.is-columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
|
|
gap: 3px 14px;
|
|
}
|
|
.tile-settings .multiselect-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
.tile-settings .multiselect-row .check-label { flex: 1 1 0; min-width: 0; }
|
|
.tile-settings .multiselect-empty { color: var(--muted); font-size: 12px; }
|
|
/* a text built from offered variables (type `template`) */
|
|
.tile-settings .template-vars { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 2px; }
|
|
.tile-settings .template-var {
|
|
font: inherit;
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
cursor: grab;
|
|
}
|
|
.tile-settings .template-var:hover { border-color: var(--accent); }
|
|
.tile-settings .template-var:active { cursor: grabbing; }
|
|
|
|
.tile-settings .field-group {
|
|
margin: 0 0 10px;
|
|
padding: 8px 10px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
.tile-settings .field-group-title {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--muted);
|
|
margin-bottom: 8px;
|
|
}
|
|
.tile-settings details.field-group > summary.field-group-title {
|
|
cursor: pointer;
|
|
list-style: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
margin-bottom: 0;
|
|
user-select: none;
|
|
}
|
|
.tile-settings details.field-group > summary::-webkit-details-marker { display: none; }
|
|
.tile-settings details.field-group > summary.field-group-title::before {
|
|
content: "";
|
|
width: 0; height: 0;
|
|
border-left: 5px solid var(--muted);
|
|
border-top: 4px solid transparent;
|
|
border-bottom: 4px solid transparent;
|
|
transition: transform 0.15s;
|
|
}
|
|
.tile-settings details.field-group[open] > summary.field-group-title::before { transform: rotate(90deg); }
|
|
.tile-settings details.field-group[open] > summary.field-group-title { margin-bottom: 8px; }
|
|
.tile-settings .field-group .field:last-child { margin-bottom: 0; }
|
|
/* Settings apply as they change, so nothing sits under the last field — its
|
|
bottom margin would only pad the popover's own padding a second time. */
|
|
.tile-settings-body > :last-child { margin-bottom: 0; }
|
|
.tile-settings .field-group-body.is-columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
|
|
gap: 6px 14px; /* the grid gap is the whole row spacing: no per-field margin */
|
|
}
|
|
.tile-settings .field-group-body.is-columns .field { margin-bottom: 0; }
|
|
.tile-settings .field-group-body.is-columns .field-group-help { grid-column: 1 / -1; }
|
|
/* a list of switches or a template with its chips is a row of its own, however
|
|
many columns the group lays its other fields out in */
|
|
.tile-settings .field-group-body.is-columns .multiselect-field,
|
|
.tile-settings .field-group-body.is-columns .template-field { grid-column: 1 / -1; }
|
|
.tile-settings .field-group-help,
|
|
.tile-settings .field-help {
|
|
display: block;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
color: var(--muted);
|
|
}
|
|
.tile-settings .field-group-help { margin: -2px 0 6px; }
|
|
.tile-settings .field-help { margin-top: 1px; }
|
|
|
|
.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);
|
|
}
|
|
/* boolean / switch settings: label on the left, switch on the right (help,
|
|
when there is any, wraps under both) */
|
|
.field.check {
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 4px 10px;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
.field.check .check-label { flex: 1 1 0; min-width: 0; }
|
|
.field.check .field-help { flex-basis: 100%; }
|
|
|
|
/* The switch: the admin page's module switch, same track, knob and colours,
|
|
so a toggle reads the same on both surfaces. On = shown/enabled (accent),
|
|
off = hidden/disabled (border track, muted knob). The real checkbox lies
|
|
invisibly over the track — still focusable, so Tab reaches it and Space
|
|
flips it — and the label around it makes the text clickable too. */
|
|
.switch {
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex: none;
|
|
cursor: pointer;
|
|
}
|
|
.switch input {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
margin: 0;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.switch .track {
|
|
display: block;
|
|
position: relative;
|
|
width: 34px;
|
|
height: 19px;
|
|
border-radius: 999px;
|
|
background: var(--border);
|
|
transition: background 0.2s;
|
|
}
|
|
.switch .track::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 15px;
|
|
height: 15px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
transition: transform 0.2s, background 0.2s;
|
|
}
|
|
.switch input:checked + .track { background: var(--accent); }
|
|
.switch input:checked + .track::after { transform: translateX(15px); background: var(--bg); }
|
|
.switch input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
|
|
/* color fields: label on the left, swatch on the right */
|
|
.field.color-field {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.field.color-field > span { flex: 1; min-width: 0; }
|
|
.field.color-field input[type="color"] {
|
|
width: 44px;
|
|
height: 26px;
|
|
padding: 2px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: var(--bg);
|
|
cursor: pointer;
|
|
}
|
|
.field.color-field input[type="color"]:focus { outline: none; border-color: var(--accent); }
|
|
.field.color-field .field-help { flex-basis: 100%; }
|
|
|
|
/* ── 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: var(--accent-text);
|
|
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; }
|
|
}
|
|
|
|
/* ── themes ────────────────────────────────────────────────────────── */
|
|
|
|
/* Preset palettes, chosen in Admin → Theme and applied by shell.js (and the
|
|
admin page) as html[data-theme="…"]. Each swaps exactly the eight palette
|
|
variables the module guide names — nothing else — so every module and
|
|
every shell element follows, and a colour a tile is configured with (set
|
|
on the tile itself) still wins. The :root block at the top of this file is
|
|
"booth", the default; the list of ids and names lives in server.js
|
|
(GET /api/theme). All four are booth-grade: high contrast, the accent
|
|
nowhere near the danger red or the warning amber, muted still readable. */
|
|
|
|
/* Harbor — deep navy, cyan accent */
|
|
html[data-theme="harbor"] {
|
|
--bg: #06101f;
|
|
--panel: #0c1a30;
|
|
--border: #1a2b48;
|
|
--text: #e6eefb;
|
|
--muted: #8ea4c8;
|
|
--accent: #2ad4ee;
|
|
--danger: #ff5a5f;
|
|
--warn: #f7b53b;
|
|
}
|
|
|
|
/* Graphite — neutral charcoal, electric-blue accent */
|
|
html[data-theme="graphite"] {
|
|
--bg: #0f1012;
|
|
--panel: #17191d;
|
|
--border: #2a2d33;
|
|
--text: #f0f2f4;
|
|
--muted: #9ca3ad;
|
|
--accent: #6cb4ff;
|
|
--danger: #ff4d4f;
|
|
--warn: #ffb224;
|
|
}
|
|
|
|
/* Ember — warm broadcast black, orange accent (danger stays a cool red,
|
|
warn a pale yellow, so the three status colours never blur) */
|
|
html[data-theme="ember"] {
|
|
--bg: #120c0a;
|
|
--panel: #1d1411;
|
|
--border: #35251e;
|
|
--text: #f6ede6;
|
|
--muted: #b39b8c;
|
|
--accent: #ff8f3a;
|
|
--danger: #f03247;
|
|
--warn: #ffd86b;
|
|
}
|
|
|
|
/* Daylight — the light one, for a front-of-house desk in daytime */
|
|
html[data-theme="daylight"] {
|
|
--bg: #eef1f5;
|
|
--panel: #ffffff;
|
|
--border: #cfd6e0;
|
|
--text: #14181f;
|
|
--muted: #5b6675;
|
|
--accent: #0b8a5f;
|
|
--accent-text: #ffffff; /* dark text drowns on this accent */
|
|
--danger: #d1242f;
|
|
--warn: #b26a00;
|
|
}
|