Send Message 1.0.0: one PIN, one place to send from; Slack 1.3.0 and ProdCom 1.4.0 become messaging providers

A new send-message module owns everything a dashboard sends: the PIN
(switchable off in admin), the unlock tokens, the Send message and Quick
replies tiles, the quick replies and quick emojis. It opens no upstream
connection of its own — it finds every module declaring
provides: ["messaging"] through the shell's module list and hands each
message to that module's POST /messaging/send over loopback, so Slack keeps
its one Slack connection and ProdCom its one ProdCom connection. Providers
check the browser's unlock token with GET /verify over loopback before
touching their upstream, so no browser on the network can post as the
location without having entered the PIN here.

Slack 1.3.0 is view-only plus reactions: the Sending group, PIN, send tiles
and quick replies are gone; it serves the messaging descriptor (channels as
recipients) and its reactions ride on the Send Message unlock. A layout's
old Send message / Quick replies tiles draw nothing until removed.

ProdCom 1.4.0 serves the descriptor as identities — only channels with
typingEnabled, since typed text goes out *as* a channel — and posts with
POST /api/v1/channels/{id}/send. Its tile is a click-to-target like Slack's
(accent stripe along the bottom), and typed entries carry a keyboard mark.

The tiles: click any Slack or ProdCom transcript to point sends there; a
Send as menu (only for platforms with identities, hidden otherwise) picks
the ProdCom channel to speak as; the choice is saved with the tile so named
layouts remember it. Wording follows the role — "Send message to #ops…" /
"Send message to ProdCom as FOH…". The PIN pad's dots ride on a real
numeric input so a phone's keyboard appears. Outcome is an icon beside
Send (check / blocked, five seconds, sentence as tooltip). A pill on the
box's bottom edge trades box height for rows of quick-emoji keys, a row
at a time; the single row scrolls sideways on a narrow tile with Send
pinned in place.

The module guide documents the provider contract and the page bus
(window.proddashMessaging + proddash:messaging:* events) the tiles share;
the ProdCom mock gained the send route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-10-05 13:55:55 -04:00
co-authored by Claude Fable 5.1
parent 3b81efcb51
commit 1274360ee6
15 changed files with 3409 additions and 1528 deletions
+527
View File
@@ -0,0 +1,527 @@
/* Send Message module — scoped to its tiles, shell variables only.
The gate (PIN pad), the compose box and the preset grid were the Slack
module's send surfaces until 1.3.0; they live here now, for every platform. */
[data-module="send-message"] .sm-wrap {
--sm-size: 14px;
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
container-type: size;
font-size: var(--sm-size);
color: var(--text);
}
[data-module="send-message"] .sm-wrap:focus { outline: none; }
[data-module="send-message"] [hidden] { display: none !important; }
/* ── the gate ── */
[data-module="send-message"] .sm-gate,
[data-module="send-message"] .sm-compose {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding: 6px 10px;
overflow: auto;
}
[data-module="send-message"] .sm-note {
margin: auto;
padding: 10px;
text-align: center;
color: var(--muted);
font-size: 0.9em;
line-height: 1.45;
}
[data-module="send-message"] .sm-note.is-error { color: var(--danger); }
/* The pad never shrinks below its content (min-height stays `auto` on it
and on the key grid) — a shrunken grid would spill its rows under the
message line, which then swallows the taps. A tile too short for it
scrolls instead (.sm-gate has overflow: auto). */
[data-module="send-message"] .sm-pad {
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 4px;
max-width: 340px;
width: 100%;
margin: 0 auto;
}
[data-module="send-message"] .sm-pad.sm-shake { animation: sm-shake 0.4s ease; }
@keyframes sm-shake {
10%, 90% { transform: translateX(-2px); }
20%, 80% { transform: translateX(3px); }
30%, 50%, 70% { transform: translateX(-5px); }
40%, 60% { transform: translateX(5px); }
}
/* one line: "Enter PIN · Slack · as Apollo Beach → #production" */
[data-module="send-message"] .sm-pad-head {
flex: none;
text-align: center;
font-size: 0.8em;
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-module="send-message"] .sm-pad-head b { font-weight: 700; color: var(--text); }
/* The dots ride on a real numeric input: tapping them focuses it, which is
what raises a phone's keyboard; on a desktop the keyboard types into it
directly. The input itself is invisible — the dots are its display. */
[data-module="send-message"] .sm-field {
flex: none;
position: relative;
height: 24px;
cursor: text;
}
[data-module="send-message"] .sm-dots {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
pointer-events: none;
}
[data-module="send-message"] .sm-dot {
width: 11px;
height: 11px;
border-radius: 50%;
border: 2px solid var(--muted);
transition: background 0.12s, border-color 0.12s;
}
[data-module="send-message"] .sm-dot.on { background: var(--accent); border-color: var(--accent); }
[data-module="send-message"] .sm-pin-input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 0;
border-radius: 8px;
background: transparent;
color: transparent;
caret-color: transparent;
font: inherit;
text-align: center;
outline: none;
-webkit-text-fill-color: transparent;
}
[data-module="send-message"] .sm-pin-input::selection { background: transparent; }
[data-module="send-message"] .sm-pin-input:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent); }
[data-module="send-message"] .sm-keys {
flex: 1 1 auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, minmax(24px, 1fr));
gap: 4px;
}
[data-module="send-message"] .sm-key {
min-width: 0;
min-height: 24px;
border: 1px solid var(--border);
border-radius: 10px;
background: color-mix(in srgb, var(--text) 5%, var(--panel));
color: var(--text);
font: inherit;
font-size: clamp(13px, 9cqh, 26px);
font-weight: 600;
font-variant-numeric: tabular-nums;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
transition: background 0.1s;
}
[data-module="send-message"] .sm-key:hover { background: var(--border); }
[data-module="send-message"] .sm-key:active { background: color-mix(in srgb, var(--accent) 30%, var(--panel)); }
[data-module="send-message"] .sm-key svg { width: 1.1em; height: 1.1em; }
[data-module="send-message"] .sm-key-back { color: var(--muted); }
[data-module="send-message"] .sm-key-blank { visibility: hidden; }
[data-module="send-message"] .sm-pad.is-busy .sm-keys { opacity: 0.6; pointer-events: none; }
[data-module="send-message"] .sm-pad-msg {
flex: none;
min-height: 1.1em;
text-align: center;
font-size: 0.78em;
line-height: 1.1;
color: var(--muted);
}
[data-module="send-message"] .sm-pad-msg.is-error { color: var(--danger); font-weight: 600; }
/* A tile too short for the keypad (the smallest Send message / Quick replies
tiles) shows the dots and a hint instead — the dots still take a tap and
the keyboard, and a taller tile brings the keys back. .sm-wrap is the size
container the height is measured against. */
[data-module="send-message"] .sm-pad-hint {
display: none;
flex: none;
text-align: center;
font-size: 0.78em;
line-height: 1.3;
color: var(--muted);
}
@container (max-height: 176px) {
[data-module="send-message"] .sm-pad .sm-keys { display: none; }
[data-module="send-message"] .sm-pad .sm-pad-hint { display: block; }
[data-module="send-message"] .sm-pad { justify-content: center; }
}
/* ── unlocked ── */
[data-module="send-message"] .sm-compose { gap: 6px; }
[data-module="send-message"] .sm-compose-head {
flex: none;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
font-size: 0.8em;
color: var(--muted);
}
[data-module="send-message"] .sm-sending {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-module="send-message"] .sm-lock-link {
flex: none;
padding: 1px 8px;
border: 1px solid var(--border);
border-radius: 999px;
background: none;
color: var(--muted);
font: inherit;
font-size: 0.9em;
cursor: pointer;
}
[data-module="send-message"] .sm-lock-link:hover { color: var(--text); background: var(--border); }
/* The box takes whatever the quick emoji rows leave it, down to one line. */
[data-module="send-message"] .sm-box {
flex: 1 1 auto;
min-height: 0;
position: relative;
display: flex;
flex-direction: column;
}
[data-module="send-message"] .sm-input {
flex: 1 1 auto;
min-height: calc(1.4em + 16px);
width: 100%;
resize: none;
padding: 7px 9px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font: inherit;
line-height: 1.4;
}
[data-module="send-message"] .sm-input:focus { outline: none; border-color: var(--accent); }
/* The grip: a small pill centred on the box's bottom edge, straddling the
border. Drag it up for more emoji rows. It is positioned over the box, so
it adds no height between the box and the keys. */
[data-module="send-message"] .sm-grip {
position: absolute;
left: 50%;
bottom: -6px;
transform: translateX(-50%);
width: 44px;
height: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: ns-resize;
touch-action: none;
z-index: 1;
-webkit-tap-highlight-color: transparent;
}
[data-module="send-message"] .sm-grip::before {
content: "";
width: 26px;
height: 4px;
border-radius: 2px;
background: var(--muted);
box-shadow: 0 0 0 1.5px var(--panel);
transition: background 0.12s, width 0.12s;
}
[data-module="send-message"] .sm-grip:hover::before,
[data-module="send-message"] .sm-grip:focus-visible::before,
[data-module="send-message"] .sm-grip.is-dragging::before { background: var(--accent); width: 34px; }
[data-module="send-message"] .sm-grip:focus { outline: none; }
/* The smallest Send message tile (two grid rows, a 110px body): the compose
area tightens its padding, gaps and the box's one-line minimum so that two
rows of quick emoji keys still fit under the box. .sm-wrap is the size
container the height is measured against. */
@container (max-height: 150px) {
[data-module="send-message"] .sm-compose { padding: 3px 10px; gap: 2px; }
[data-module="send-message"] .sm-input { padding: 4px 9px; line-height: 1.25; min-height: calc(1.25em + 10px); }
}
/* The Send button and the outcome icon sit right under the box, level with
the first row of keys; further rows of keys run on below them. */
[data-module="send-message"] .sm-compose-foot {
flex: none;
display: flex;
align-items: flex-start;
gap: 8px;
min-height: 1.6em;
min-width: 0;
}
/* the outcome of a send, as an icon beside the Send button: a check (accent
— green on the default theme) or a blocked sign (danger), fading out as
its five seconds run down; the sentence is the tooltip. The slot keeps its
width while empty so the Send button never shifts. */
[data-module="send-message"] .sm-feedback {
flex: none;
width: 1.45em;
min-height: 2.1em; /* the Send button's height, so the icon centres on it */
display: flex;
align-items: center;
justify-content: center;
}
[data-module="send-message"] .sm-countdown { align-self: center; }
[data-module="send-message"] .sm-feedback svg {
width: 1.45em;
height: 1.45em;
flex: none;
animation: sm-pop 0.18s ease-out, sm-fade 0.6s ease-in 4.4s forwards;
}
[data-module="send-message"] .sm-feedback.is-ok { color: var(--accent); }
[data-module="send-message"] .sm-feedback.is-error { color: var(--danger); cursor: help; }
/* the Quick replies foot has no Send button: the icon keeps to the right edge */
[data-module="send-message"] .sm-compose-foot.is-quick { justify-content: flex-end; }
@keyframes sm-pop {
from { transform: scale(0.6); opacity: 0; }
to { transform: none; opacity: 1; }
}
@keyframes sm-fade {
to { opacity: 0; }
}
[data-module="send-message"] .sm-countdown {
flex: none;
font-size: 0.75em;
color: var(--warn);
font-variant-numeric: tabular-nums;
}
[data-module="send-message"] .sm-sendbtn {
flex: none;
padding: 6px 16px;
border: none;
border-radius: 8px;
background: var(--accent);
color: var(--accent-text);
font: inherit;
font-weight: 700;
cursor: pointer;
}
[data-module="send-message"] .sm-sendbtn:hover { background: color-mix(in srgb, var(--accent) 85%, var(--text)); }
[data-module="send-message"] .sm-sendbtn:disabled { opacity: 0.6; cursor: default; }
/* quick emojis: one press each sends it as a message of its own. The keys
take whatever room the foot has to the left of the outcome icon and the
Send button. As one row (the default) they scroll sideways when the tile
is narrow (trackpad, touch, or the wheel — client.js turns a vertical
wheel into a horizontal scroll) while Send keeps its place; with more
rows allotted from the grip they wrap, --sm-quick-rows rows tall. One key
is --sm-key square, so a row's pitch is --sm-key plus the 4px gap. */
[data-module="send-message"] .sm-emoji-sendrow {
--sm-quick-rows: 1;
--sm-key: calc(2.31 * var(--sm-size));
flex: 1 1 auto;
display: flex;
gap: 4px;
min-width: var(--sm-key);
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
[data-module="send-message"] .sm-emoji-sendrow.is-multi {
flex-wrap: wrap;
align-content: flex-start;
height: calc(var(--sm-quick-rows) * var(--sm-key) + (var(--sm-quick-rows) - 1) * 4px);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
[data-module="send-message"] .sm-emoji-sendrow::-webkit-scrollbar { display: none; }
/* more keys off one edge or the other: that edge fades out (client.js sets the classes) */
[data-module="send-message"] .sm-emoji-sendrow.has-more { mask-image: linear-gradient(to right, #000 calc(100% - 16px), transparent); }
[data-module="send-message"] .sm-emoji-sendrow.has-less { mask-image: linear-gradient(to right, transparent, #000 16px); }
[data-module="send-message"] .sm-emoji-sendrow.has-less.has-more { mask-image: linear-gradient(to right, transparent, #000 16px, #000 calc(100% - 16px), transparent); }
[data-module="send-message"] .sm-emoji-sendrow:empty { display: none; }
[data-module="send-message"] .sm-emoji-send {
flex: none;
width: var(--sm-key);
height: var(--sm-key);
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in srgb, var(--text) 5%, var(--panel));
color: var(--text);
font: inherit;
font-size: 1.1em;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
transition: background 0.1s, border-color 0.1s;
}
[data-module="send-message"] .sm-emoji-send:hover { border-color: var(--accent); background: var(--border); }
[data-module="send-message"] .sm-emoji-send:active { background: color-mix(in srgb, var(--accent) 30%, var(--panel)); }
[data-module="send-message"] .sm-emoji-send.is-busy { opacity: 0.6; }
[data-module="send-message"] .sm-emoji-send.is-sent { background: var(--accent); border-color: transparent; }
[data-module="send-message"] .sm-emoji-char { font-size: 1em; line-height: 1; }
/* a :code: or a word rather than one emoji: a wider pill of small text */
[data-module="send-message"] .sm-emoji-send.is-text {
width: auto;
max-width: 7em;
padding: 0 7px;
}
[data-module="send-message"] .sm-emoji-send.is-text .sm-emoji-char {
font-size: 0.72em;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* the preset grid: big targets, labels wrap */
[data-module="send-message"] .sm-grid {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr));
grid-auto-rows: minmax(44px, auto);
gap: 6px;
align-content: start;
overflow-y: auto;
}
[data-module="send-message"] .sm-grid .sm-note { grid-column: 1 / -1; }
[data-module="send-message"] .sm-quick {
min-width: 0;
min-height: 44px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 10px;
background: color-mix(in srgb, var(--accent) 12%, var(--panel));
color: var(--text);
font: inherit;
font-weight: 600;
line-height: 1.25;
text-align: center;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
transition: background 0.1s;
}
[data-module="send-message"] .sm-quick:hover { background: color-mix(in srgb, var(--accent) 22%, var(--panel)); }
[data-module="send-message"] .sm-quick:active { background: color-mix(in srgb, var(--accent) 40%, var(--panel)); }
[data-module="send-message"] .sm-quick.is-busy { opacity: 0.7; }
[data-module="send-message"] .sm-quick.is-sent {
background: var(--accent);
border-color: transparent;
color: var(--accent-text);
}
[data-module="send-message"] .sm-quick-label { overflow-wrap: anywhere; }
[data-module="send-message"] .sm-quick-chan {
font-size: 0.72em;
font-weight: 500;
color: var(--muted);
}
/* sent: the words step aside for a check, in the button itself */
[data-module="send-message"] .sm-quick { position: relative; }
[data-module="send-message"] .sm-quick-check {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
}
[data-module="send-message"] .sm-quick-check svg { width: 1.6em; height: 1.6em; animation: sm-pop 0.18s ease-out; }
[data-module="send-message"] .sm-quick.is-sent .sm-quick-label,
[data-module="send-message"] .sm-quick.is-sent .sm-quick-chan { visibility: hidden; }
[data-module="send-message"] .sm-quick.is-sent .sm-quick-check { display: flex; }
/* ── the Send as menu ──
The menu container is created and positioned by the shell (.tile-menu);
it hangs off the body under a [data-module] host, so scoping still applies. */
[data-module="send-message"] .sm-menu { min-width: 180px; max-height: min(60vh, 420px); overflow-y: auto; }
[data-module="send-message"] .sm-menu-head {
padding: 6px 9px 2px;
font-size: 11px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-module="send-message"] .sm-menu-head b { color: var(--text); font-weight: 700; }
[data-module="send-message"] .sm-menu-head.is-error b { color: var(--danger); }
[data-module="send-message"] .sm-menu-head.is-muted b { color: var(--muted); }
[data-module="send-message"] .sm-menu-head + .sm-menu-head,
[data-module="send-message"] .sm-menu-item + .sm-menu-head,
[data-module="send-message"] .sm-menu-note + .sm-menu-head {
margin-top: 4px;
border-top: 1px solid var(--border);
padding-top: 8px;
}
[data-module="send-message"] .sm-menu-note {
padding: 4px 9px 6px;
font-size: 11px;
color: var(--muted);
max-width: 260px;
line-height: 1.35;
}
[data-module="send-message"] .sm-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 6px 9px;
background: none;
border: none;
border-radius: 6px;
color: var(--text);
font-size: 12px;
font-family: inherit;
text-align: left;
cursor: pointer;
white-space: nowrap;
}
[data-module="send-message"] .sm-menu-item:hover { background: var(--border); }
[data-module="send-message"] .sm-menu-item .check {
width: 14px;
flex: none;
color: var(--accent);
font-weight: 700;
visibility: hidden;
}
[data-module="send-message"] .sm-menu-item.on .check { visibility: visible; }
[data-module="send-message"] .sm-menu-item.remembered .check { visibility: visible; color: var(--muted); opacity: 0.6; }
[data-module="send-message"] .sm-menu-item .name { font-weight: 600; }
[data-module="send-message"] .sm-menu-item.on .name { color: var(--accent); }