refactor(ui): overlay the status icons and let the preview fill the panel

Three adjustments to the panel layout:

- the timer status icons go back to overlaying the stage, which returns the
  row they were costing. They sit in the same chrome layer as the pop-out
  buttons, so the blackout no longer hides them
- the screen state buttons group with the preview they act on, and the gap
  before the message inputs widens to separate the two
- the stage drops its fixed aspect ratio and fills the panel width, which
  also removes the slack a tall panel used to leave around it

Without a 16:9 frame the width derived font size can overflow a short, wide
stage, so the timer is now capped by container height at the ratio the
estimate assumes. In the pip window, which has no ancestor query container,
both terms resolve against the viewport, so a 16:9 window is unchanged and a
wider one stops clipping its digits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011CHxSQ6nWHuyar8feieRra
This commit is contained in:
Carlos Valente
2026-08-26 13:30:28 +00:00
parent 0d5228f1e5
commit c4ac9df8cf
7 changed files with 32 additions and 36 deletions
@@ -0,0 +1,8 @@
/* the screen state buttons belong with the preview they act on, not with the message inputs */
.screenGroup {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
gap: $element-inner-spacing;
}
@@ -8,11 +8,15 @@ import InputRow from './InputRow';
import ScreenControl from './ScreenControl';
import TimerPreview from './TimerPreview';
import style from './MessageControl.module.scss';
export default function MessageControl() {
return (
<>
<TimerPreview />
<ScreenControl />
<div className={style.screenGroup}>
<TimerPreview />
<ScreenControl />
</div>
<TimerMessageInput />
<SecondaryInput />
</>
@@ -18,7 +18,7 @@
display: flex;
flex-direction: column;
gap: $element-spacing;
gap: $section-spacing;
color: $ui-white;
padding-top: $panel-top-padding;
}
@@ -1,36 +1,17 @@
.preview {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
/* the stage caps at the panel width, so centre whatever slack a tall panel leaves */
justify-content: center;
gap: $element-inner-spacing;
}
/* the stage is the only elastic element in the panel, the controls below it keep their size */
.stage {
flex: 1 1 auto;
min-height: 3rem;
/* keep the height driven frame from growing wider than the panel */
max-height: calc(100cqw * 9 / 16);
display: grid;
place-items: center;
}
/* the frame is a query container so that the embedded timer scales to the preview, not to the viewport */
.stageFrame {
position: relative;
container-type: inline-size;
/* a query container so that the embedded timer scales to the preview, not to the viewport */
container-type: size;
contain: paint;
overflow: hidden;
aspect-ratio: 16 / 9;
height: 100%;
border-radius: $component-border-radius-md;
}
/* keep the pop-out affordances reachable above the blackout and message overlays */
.stageCorners {
/* editor chrome sits above the blackout and message overlays, so it stays reachable and readable */
.stageChrome {
position: absolute;
inset: 0;
z-index: calc($zindex-floating + 2);
@@ -11,16 +11,12 @@ export default function TimerPreview() {
const { data } = useViewSettings();
return (
<div className={style.preview}>
<div className={style.stage}>
<div className={style.stageFrame}>
<PipTimer viewSettings={data} />
<div className={style.stageCorners}>
<CornerWithPip onExtractClick={(event) => handleLinks('timer', event)} pipElement={<PipRoot />} />
</div>
</div>
<div className={style.stage}>
<PipTimer viewSettings={data} />
<div className={style.stageChrome}>
<TimerStatus />
<CornerWithPip onExtractClick={(event) => handleLinks('timer', event)} pipElement={<PipRoot />} />
</div>
<TimerStatus />
</div>
);
}
@@ -1,5 +1,10 @@
.timerStatus {
position: absolute;
top: 0;
left: 0;
margin: 0.5rem 0.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
@@ -64,7 +64,9 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
// gather presentation styles
const resolvedTimerColour = getTimerColour(viewSettings, undefined, showWarning, showDanger);
// the estimate is tuned for a 16:9 screen, so cap it by height for containers wider than that
const timerFontSize = getEstimatedFontSize(display, secondaryContent);
const timerFontRule = `min(${timerFontSize}cqw, ${((timerFontSize * 16) / 9).toFixed(2)}cqh)`;
const userStyles = {
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
};
@@ -82,7 +84,7 @@ export function PipTimer({ viewSettings }: PipTimerProps) {
<div className={cx(['timer-container', message.timer.blink && !showOverlay && 'blink'])}>
<div
className={cx(['timer', !isPlaying && 'timer--paused', showFinished && 'timer--finished'])}
style={{ fontSize: `${timerFontSize}cqw` }}
style={{ fontSize: timerFontRule }}
data-phase={time.phase}
>
{display}