From 648abfa3f7c91395b5eeca543f70e0b6ea59e9e0 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:54:23 +0800 Subject: [PATCH 1/2] docs: align readiness runtime version --- docs/production-readiness-checklist.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/production-readiness-checklist.md b/docs/production-readiness-checklist.md index e38e98c7f..1596dc639 100644 --- a/docs/production-readiness-checklist.md +++ b/docs/production-readiness-checklist.md @@ -2,9 +2,9 @@ This is the runbook to make the app publishable in one focused pass. -Last reviewed: 2026-07-04. Applies to any feature branch or release candidate. +Last reviewed: 2026-07-10. Applies to any feature branch or release candidate. -- Runtime target: Next.js 16.2.9, Node 24.x, npm 11.x. +- Runtime target: Next.js 16.2.10, Node 24.x, npm 11.x. - Supabase target: `sjrfecxgysukkwxsowpy` (`Clinical KB Database`). ## Immediate completion targets From 31b055d177121459b516457fb8649a62dee0f8a3 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:48:22 +0800 Subject: [PATCH 2/2] redesign(composer): neutral desktop plus-menu popover with in-place mode picker The >=1024px "+" popover header crushed its subtitle to an ellipsis (grid reserved 216px for the mode pill inside a 22rem surface) and still carried the legacy teal-gradient styling. Rebuild it to the Sheet header anatomy the mobile sheet already uses: accent icon tile, mode title as the switcher trigger, and the subtitle on its own full-width line so it can never truncate. - swap the layered mode dropdown for an in-place body swap; the old dropdown was clipped by the panel overflow when the current mode had few action rows, leaving below-fold modes unclickable - single anchor caret centred on the trigger via --mode-action-caret-left (removes the duplicate JSX carets); widen the popover 22rem -> 24rem - drop the gradient header CSS and the unreachable <=430px mode-action rules (the popover only renders >=1024px; smaller viewports use the Sheet) - keep renderPopoverBody in statement form: react-hooks/refs flags ref writes inside render helpers invoked from conditional JSX expressions Co-Authored-By: Claude Fable 5 --- src/app/globals.css | 216 +----------------- .../clinical-dashboard/mode-action-popup.tsx | 173 +++++++------- 2 files changed, 105 insertions(+), 284 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index e7962a80b..22490d9e3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -828,11 +828,14 @@ summary::-webkit-details-marker { transform-origin: center top; } +/* Single anchor caret, centred on the "+" trigger. Detached surfaces are anchored + to the composer pill so a fixed offset works; integrated surfaces pass the exact + trigger centre via --mode-action-caret-left (set inline from updatePlacement). */ .mode-action-surface[data-placement="up"]::after { content: ""; position: absolute; bottom: -0.42rem; - left: 50%; + left: var(--mode-action-caret-left, 2.4rem); z-index: 2; width: 0.9rem; height: 0.9rem; @@ -849,7 +852,7 @@ summary::-webkit-details-marker { content: ""; position: absolute; top: -0.42rem; - left: 50%; + left: var(--mode-action-caret-left, 2.4rem); z-index: 2; width: 0.9rem; height: 0.9rem; @@ -858,11 +861,7 @@ summary::-webkit-details-marker { border-right: 0; border-bottom: 0; border-top-left-radius: 0.18rem; - background: linear-gradient( - 135deg, - color-mix(in srgb, var(--clinical-accent) 78%, #ffffff 22%) 0%, - color-mix(in srgb, var(--primary-700) 82%, #ffffff 18%) 100% - ); + background: var(--surface-raised); box-shadow: -5px -5px 14px rgb(15 37 48 / 6%); } @@ -882,154 +881,17 @@ summary::-webkit-details-marker { border-top-right-radius: 1rem; } -.mode-action-header { - position: relative; - z-index: 1; - display: grid; - min-height: 4.9rem; - grid-template-areas: "selector summary close"; - grid-template-columns: minmax(13.5rem, 0.42fr) minmax(0, 1fr) 3.25rem; - align-items: center; - gap: 0.75rem; - padding: 0.72rem 0.78rem; - color: #fff; - background: - radial-gradient(circle at 18% 0%, rgb(255 255 255 / 18%), transparent 34%), - linear-gradient( - 135deg, - color-mix(in srgb, var(--clinical-accent) 88%, #073d4a 12%) 0%, - color-mix(in srgb, var(--primary-700) 86%, #ffffff 14%) 100% - ); - box-shadow: - inset 0 1px 0 rgb(255 255 255 / 18%), - inset 0 -1px 0 rgb(255 255 255 / 10%); -} - -.mode-action-selector-shell { - position: relative; - grid-area: selector; - min-width: 0; -} - -.mode-action-mode-button { - display: grid; - min-height: 3.15rem; - width: 100%; - min-width: 0; - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: center; - gap: 0.75rem; - border: 1px solid rgb(255 255 255 / 30%); - border-radius: 999px; - background: rgb(255 255 255 / 10%); - padding: 0.42rem 0.72rem; - color: #fff; - font-size: 1rem; - font-weight: 850; - line-height: 1; - text-align: left; - box-shadow: - inset 0 1px 0 rgb(255 255 255 / 18%), - 0 8px 22px rgb(0 35 44 / 14%); - transition: - background-color 160ms ease, - border-color 160ms ease; -} - -.mode-action-mode-button:hover { - border-color: rgb(255 255 255 / 42%); - background: rgb(255 255 255 / 14%); -} - -.mode-action-mode-button:disabled { - cursor: default; -} - -.mode-action-mode-button:focus-visible, -.mode-action-close:focus-visible, .mode-action-mode-option:focus-visible { - outline: 2px solid color-mix(in srgb, #fff 70%, var(--clinical-accent)); + outline: 2px solid var(--focus); outline-offset: 2px; } -.mode-action-mode-icon { - display: grid; - height: 2.1rem; - width: 2.1rem; - flex: 0 0 auto; - place-items: center; - border: 1px solid rgb(255 255 255 / 32%); - border-radius: 0.5rem; - background: rgb(255 255 255 / 12%); - color: #fff; -} - -.mode-action-header-summary { - grid-area: summary; - display: flex; - min-width: 0; - align-items: center; - gap: 1rem; - color: rgb(255 255 255 / 92%); - font-size: 0.98rem; - font-weight: 720; - line-height: 1.2; -} - -.mode-action-header-divider { - display: block; - height: 2.15rem; - width: 1px; - flex: 0 0 auto; - background: rgb(255 255 255 / 28%); -} - -.mode-action-close { - grid-area: close; - display: grid; - height: 2.7rem; - width: 2.7rem; - place-items: center; - justify-self: center; - border: 1px solid rgb(255 255 255 / 36%); - border-radius: 999px; - background: rgb(255 255 255 / 10%); - color: #fff; - box-shadow: inset 0 1px 0 rgb(255 255 255 / 16%); - transition: - background-color 160ms ease, - border-color 160ms ease; -} - -.mode-action-close:hover { - border-color: rgb(255 255 255 / 48%); - background: rgb(255 255 255 / 16%); -} - .mode-action-body { max-height: var(--mode-action-body-max-height); overflow-y: auto; overscroll-behavior: contain; } -.mode-action-mode-menu { - position: absolute; - top: calc(100% + 0.45rem); - left: 0; - z-index: 20; - width: min(22rem, calc(100vw - 2rem)); - max-height: min(19rem, var(--mode-action-body-max-height)); - overflow-y: auto; - border: 1px solid var(--border-lux); - border-radius: 0.75rem; - background: color-mix(in srgb, var(--surface-lux) 96%, transparent); - padding: 0.4rem; - color: var(--text); - box-shadow: 0 18px 48px rgb(15 37 48 / 18%); - backdrop-filter: blur(18px) saturate(140%); - -webkit-backdrop-filter: blur(18px) saturate(140%); -} - .mode-action-mode-option { display: grid; min-height: 3.2rem; @@ -1071,69 +933,9 @@ summary::-webkit-details-marker { color: var(--clinical-accent); } +/* The mode-action popover only renders ≥1024px (smaller viewports get the Sheet), + so this block holds no .mode-action-* overrides. */ @media (max-width: 430px) { - .mode-action-header { - min-height: 3.7rem; - grid-template-areas: "selector close"; - grid-template-columns: minmax(0, 1fr) 2.35rem; - gap: 0.45rem; - padding: 0.48rem; - } - - .mode-action-mode-button { - min-height: 2.25rem; - gap: 0.42rem; - padding-inline: 0.5rem; - font-size: 0.9rem; - } - - .mode-action-mode-icon { - height: 1.45rem; - width: 1.45rem; - border-radius: 0.42rem; - } - - .mode-action-mode-icon svg, - .mode-action-mode-option-icon svg { - height: 0.9rem; - width: 0.9rem; - } - - .mode-action-header-summary { - display: none; - } - - .mode-action-header-divider { - display: none; - } - - .mode-action-close { - height: 2.05rem; - width: 2.05rem; - } - - .mode-action-close svg { - height: 0.95rem; - width: 0.95rem; - } - - .mode-action-mode-menu { - width: min(20rem, calc(100vw - 1.5rem)); - } - - .mode-action-mode-option { - min-height: 2.75rem; - grid-template-columns: 1.75rem minmax(0, 1fr) auto; - gap: 0.5rem; - padding: 0.38rem 0.45rem; - } - - .mode-action-mode-option-icon { - height: 1.7rem; - width: 1.7rem; - border-radius: 0.42rem; - } - .answer-footer-search-action, .answer-footer-search-send { height: 2.75rem; diff --git a/src/components/clinical-dashboard/mode-action-popup.tsx b/src/components/clinical-dashboard/mode-action-popup.tsx index 1a5e5275f..c5378ab17 100644 --- a/src/components/clinical-dashboard/mode-action-popup.tsx +++ b/src/components/clinical-dashboard/mode-action-popup.tsx @@ -42,7 +42,7 @@ import { import { useDismissableLayer } from "@/components/use-dismissable-layer"; import { Sheet } from "@/components/ui/sheet"; -import { cn, chatComposerIconButton } from "@/components/ui-primitives"; +import { cn, chatComposerIconButton, toolbarButton } from "@/components/ui-primitives"; export type ModeActionSetId = "answer" | "documents" | "services" | "forms" | "favourites" | "tools" | "differentials" | "prescribing"; @@ -52,6 +52,7 @@ type IntegratedSurfaceLayout = { placement: ModeActionPlacement; left: number; width: number; + caretLeft: number; top?: number; bottom?: number; }; @@ -63,7 +64,7 @@ function estimateActionListHeights(itemCount: number, integrated: boolean) { const rowGap = 6; const bodyPadding = integrated ? 24 : 20; const minBodyHeight = itemCount * rowHeight + Math.max(0, itemCount - 1) * rowGap + bodyPadding; - const headerHeight = 92; + const headerHeight = 76; return { minBodyHeight, minSurfaceHeight: minBodyHeight + headerHeight, headerHeight }; } @@ -387,12 +388,15 @@ export function ModeActionPopup({ setBodyMaxHeight((current) => (current === nextBodyMaxHeight ? current : nextBodyMaxHeight)); if (integrated) { - const maxSurfaceWidth = Math.min(window.innerWidth - edgePadding * 2, 360); + const maxSurfaceWidth = Math.min(window.innerWidth - edgePadding * 2, 384); const surfaceLeft = Math.max(edgePadding, Math.min(rect.left, window.innerWidth - maxSurfaceWidth - edgePadding)); setIntegratedSurfaceLayout({ placement: nextPlacement, left: surfaceLeft, width: maxSurfaceWidth, + // Keep the caret centred on the "+" trigger even when the surface is + // clamped to the viewport edge and no longer starts at the trigger. + caretLeft: Math.max(20, rect.left - surfaceLeft + rect.width / 2), ...(nextPlacement === "up" ? { bottom: window.innerHeight - rect.top + 14 } : { top: rect.bottom + integratedDownOffset }), @@ -546,6 +550,7 @@ export function ModeActionPopup({ ? { left: `${integratedSurfaceLayout.left}px`, width: `${integratedSurfaceLayout.width}px`, + "--mode-action-caret-left": `${integratedSurfaceLayout.caretLeft}px`, ...(integratedSurfaceLayout.top !== undefined ? { top: `${integratedSurfaceLayout.top}px` } : {}), ...(integratedSurfaceLayout.bottom !== undefined ? { bottom: `${integratedSurfaceLayout.bottom}px` } : {}), } @@ -625,12 +630,79 @@ export function ModeActionPopup({ ); } - // Desktop popover header: the mode name doubles as the mode switcher, with the - // subtitle and a close control. Sheets use the Sheet component's own header. + // In-place mode picker: while the header's mode switcher is open, the popover + // body swaps the action rows for this list (same scroll area, so it can never + // be clipped by the panel the way a layered dropdown was). Escape or a second + // press of the title returns to the actions. + function renderModeList() { + if (!modeOptions?.length) return null; + return ( + + ); + } + + // Statement form (not a JSX ternary): react-hooks/refs treats helpers invoked + // conditionally inside JSX as possible non-render callbacks and then flags the + // ref writes/handlers inside both list renderers. + function renderPopoverBody() { + if (modeSelectorOpen && modeOptions?.length) return renderModeList(); + return renderActionRows(); + } + + // Desktop popover header — same anatomy as the Sheet header the ≤1023px surface + // uses (accent icon tile + stacked title/subtitle + close), so the menu reads as + // one design across breakpoints. The title doubles as the mode switcher; the + // subtitle sits on its own full-width line so it can never be crushed. function renderPopoverHeader() { return ( -
-
+
+ +
- {modeSelectorOpen && modeOptions?.length ? ( - - ) : null} -
-
-
); @@ -732,7 +766,7 @@ export function ModeActionPopup({ ? integratedDownOffsetClass : "top-[calc(100%+0.875rem)]" : null, - !integrated && "sm:w-[min(22rem,100%)]", + !integrated && "sm:w-[min(24rem,100%)]", )} >
{renderPopoverHeader()} - {renderActionRows()} + {renderPopoverBody()}
- {!integrated ? ( - <> - {placement === "up" ? ( -
) : null;