From 99f8f4885ca57c17fae3ed16af54d5070f8a4590 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Wed, 26 Aug 2026 13:36:52 +0000 Subject: [PATCH] feat(ui): make blink/blackout exclusive and put the buttons beside the preview Blink and blackout are two ways of hiding the same timer, and having both on does not mean anything the audience can read differently from either alone. Turning one on now turns the other off, enforced where the two toggles are sent so the invariant holds regardless of which control calls it. The screen buttons move from a row below the stage to a column on its right, which is where they act and costs less height than a row. The stage and the button column live in a grid so the buttons can be a fixed, content-sized width instead of stretching to match the stage's height. A container query against the panel's own width collapses back to a stacked layout, buttons in a row below the stage, for a narrower fit (a resized /messagecontrol window, for instance) where the two can't sit side by side. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011CHxSQ6nWHuyar8feieRra --- apps/client/src/common/hooks/useSocket.ts | 7 +++++-- .../control/message/MessageControl.module.scss | 18 ++++++++++++++---- .../control/message/ScreenControl.module.scss | 14 ++++++++++++-- .../control/message/TimerPreview.module.scss | 2 +- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 30862c92e..49df2b066 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -51,8 +51,11 @@ export const setMessage = { timerText: (payload: string) => sendSocket('message', { timer: { text: payload } }), timerVisible: (payload: boolean) => sendSocket('message', { timer: { visible: payload } }), secondaryMessage: (payload: string) => sendSocket('message', { secondary: payload }), - timerBlink: (payload: boolean) => sendSocket('message', { timer: { blink: payload } }), - timerBlackout: (payload: boolean) => sendSocket('message', { timer: { blackout: payload } }), + // blink and blackout are mutually exclusive stage states, so turning one on turns the other off + timerBlink: (payload: boolean) => + sendSocket('message', payload ? { timer: { blink: true, blackout: false } } : { timer: { blink: false } }), + timerBlackout: (payload: boolean) => + sendSocket('message', payload ? { timer: { blackout: true, blink: false } } : { timer: { blackout: false } }), timerSecondarySource: (payload: TimerMessage['secondarySource']) => sendSocket('message', { timer: { secondarySource: payload } }), /** returns the stage to a plain timer, keeping whatever the operator has typed */ diff --git a/apps/client/src/features/control/message/MessageControl.module.scss b/apps/client/src/features/control/message/MessageControl.module.scss index 427b13344..9561340a5 100644 --- a/apps/client/src/features/control/message/MessageControl.module.scss +++ b/apps/client/src/features/control/message/MessageControl.module.scss @@ -1,8 +1,18 @@ -/* the screen state buttons belong with the preview they act on, not with the message inputs */ +/* the screen state buttons belong with the preview they act on, not with the message inputs. + buttons sit in a column to the right of the stage by default; once the container narrows + too far for that (e.g. an extracted window resized narrow), they drop into a row below it */ .screenGroup { flex: 1 1 auto; min-height: 0; - display: flex; - flex-direction: column; - gap: $element-inner-spacing; + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: 1fr; + gap: $element-spacing; +} + +@container (max-width: 22rem) { + .screenGroup { + grid-template-columns: 1fr; + grid-template-rows: 1fr auto; + } } diff --git a/apps/client/src/features/control/message/ScreenControl.module.scss b/apps/client/src/features/control/message/ScreenControl.module.scss index 43b722d2b..fae239d69 100644 --- a/apps/client/src/features/control/message/ScreenControl.module.scss +++ b/apps/client/src/features/control/message/ScreenControl.module.scss @@ -1,6 +1,16 @@ .screenControl { display: grid; - grid-auto-flow: column; - grid-auto-columns: 1fr; + grid-auto-flow: row; gap: $element-spacing; + align-self: center; + width: 8rem; +} + +@container (max-width: 22rem) { + .screenControl { + grid-auto-flow: column; + grid-auto-columns: 1fr; + align-self: stretch; + width: 100%; + } } diff --git a/apps/client/src/features/control/message/TimerPreview.module.scss b/apps/client/src/features/control/message/TimerPreview.module.scss index 002a8ec3e..898ae4785 100644 --- a/apps/client/src/features/control/message/TimerPreview.module.scss +++ b/apps/client/src/features/control/message/TimerPreview.module.scss @@ -1,6 +1,6 @@ /* the stage is the only elastic element in the panel, the controls below it keep their size */ .stage { - flex: 1 1 auto; + min-width: 0; min-height: 3rem; position: relative; /* a query container so that the embedded timer scales to the preview, not to the viewport */