/** * Pure geometry for the floating window primitive ( sub-task 2). * * NO DOM and NO React on purpose: jsdom reports zero for every bounding box, so geometry asserted * through a render would agree with any implementation. Keeping the arithmetic here is what makes it * genuinely testable. */ export type WindowMode = 'fullscreen' | 'windowed' | 'minimised'; export interface Rect { x: number; y: number; width: number; height: number } export interface Viewport { width: number; height: number } export const MIN_WIDTH = 280; export const MIN_HEIGHT = 180; /** How much of the window must stay reachable. Dragging fully off-screen is unrecoverable. */ export const KEEP_ON_SCREEN = 48; /** Below this width the window is forced fullscreen and drag/resize are disabled. */ export const NARROW_BREAKPOINT = 768; export function applyMinimumSize(rect: Rect): Rect { return { ...rect, width: Math.max(MIN_WIDTH, rect.width), height: Math.max(MIN_HEIGHT, rect.height), }; } export function clampRect(rect: Rect, viewport: Viewport): Rect { // Math.max(0,...) matters: on a viewport narrower than KEEP_ON_SCREEN the bound would go negative // and clamp the window off the top-left instead of onto the screen. const maxX = Math.max(0, viewport.width - KEEP_ON_SCREEN); const maxY = Math.max(0, viewport.height - KEEP_ON_SCREEN); return { ...rect, x: Math.min(Math.max(0, rect.x), maxX), y: Math.min(Math.max(0, rect.y), maxY), }; } export function moveRect(rect: Rect, dx: number, dy: number, viewport: Viewport): Rect { return clampRect({ ...rect, x: rect.x + dx, y: rect.y + dy }, viewport); } export function defaultRect(viewport: Viewport): Rect { const width = Math.max(MIN_WIDTH, Math.round(viewport.width * 0.5)); const height = Math.max(MIN_HEIGHT, Math.round(viewport.height * 0.6)); return { x: Math.max(0, Math.round((viewport.width - width) / 2)), y: Math.max(0, Math.round((viewport.height - height) / 2)), width, height, }; } /** Breathing room between a side-anchored window and the viewport edge. */ export const SIDE_MARGIN = 24; /** * WHERE A WINDOW SHOULD OPEN WHEN IT MUST NOT COVER THE APP'S LEFT COLUMN. * * THE DEFECT. `defaultRect` above centres on the VIEWPORT, while the app's content is centred inside * a `max-w-[1600px]` frame. Those two centres are not the same place on a wide monitor, and the gap is * exactly where the left column lives: on a 3440px display the centred window spans x=8602580 while the * collapsed queue rail sits at roughly x=920984 - **inside it**. A clinician with notes open could not * click *Expand queue* to reach their own patient list without minimising first, every single time. * * THE WINDOW WAS ALWAYS DRAGGABLE. That is not the point: a default that has to be corrected on every * open is the defect, not the absence of a workaround. * * Anchored to the RIGHT with a margin, so it clears a left column at any viewport width rather than at * one that happened to be measured. On a wide display it lands in the empty margin beside the 1600px * content frame and covers nothing at all; as the viewport narrows it moves onto the workspace, which is * the content the note is ABOUT and the one thing a floating note may reasonably sit over. * * It is CLAMPED, not merely computed. "Moved it so the queue is clickable" is also satisfied by pushing * the window off-screen, which is unrecoverable - `clampRect` is what makes the result usable rather than * just absent, and below `NARROW_BREAKPOINT` the primitive forces fullscreen anyway. */ export function sideAnchoredRect(viewport: Viewport): Rect { const width = Math.max(MIN_WIDTH, Math.round(viewport.width * 0.34)); const height = Math.max(MIN_HEIGHT, Math.round(viewport.height * 0.6)); return clampRect( { x: Math.max(0, viewport.width - width - SIDE_MARGIN), y: SIDE_MARGIN, width, height, }, viewport, ); } export type ResizeEdge = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'; export const RESIZE_EDGES: readonly ResizeEdge[] = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'] as const; /** * Resize from one edge/corner. * * The minimum is enforced PER EDGE, not by a post-hoc `applyMinimumSize`. On a west/north drag the * position moves too, so clamping the size afterwards would leave the OPPOSITE edge drifting - the user * drags the left edge right and watches the right edge slide left with it. */ export function resizeRect(rect: Rect, edge: ResizeEdge, dx: number, dy: number): Rect { let { x, y, width, height } = rect; if (edge.includes('e')) { width = Math.max(MIN_WIDTH, width + dx); } if (edge.includes('w')) { const right = x + width; // pin the edge that is NOT being dragged width = Math.max(MIN_WIDTH, width - dx); x = right - width; } if (edge.includes('s')) { height = Math.max(MIN_HEIGHT, height + dy); } if (edge.includes('n')) { const bottom = y + height; // pin the edge that is NOT being dragged height = Math.max(MIN_HEIGHT, height - dy); y = bottom - height; } return { x, y, width, height }; }