mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(coach): dock the conversations panel flush against the right edge
The Coach page cancelled the shell's container padding with negative margins, so <main> kept its reserved scrollbar gutter and painted a dead strip to the right of the panel on classic-scrollbar platforms. The shell now renders /coach full bleed: no centred container, no padding and no reserved gutter, and the page fills what <main> leaves below the top bar. A refusal card (shared record, module off) keeps the padded frame. The panel header takes one 12 px inset with a gap of 8 between title and icons, a text-lg title, and 28 px icon buttons beside a pointer (44 px on touch). Conversation rows take the control radius and a 36 px pointer floor.
This commit is contained in:
1 parent
854cd31e18
commit
c49a04bac6
6 files changed
+97
-30
No files matched your search
@@ -146,14 +146,14 @@ export function ConversationsPanel({
|
||||
const header = (inSheet: boolean) => (
|
||||
<div
|
||||
data-slot="coach-conversations-panel-header"
|
||||
className="border-border flex h-14 shrink-0 items-center gap-1 border-b pr-2 pl-3"
|
||||
className="border-border flex shrink-0 items-center gap-2 border-b p-3"
|
||||
>
|
||||
{inSheet ? (
|
||||
<SheetTitle className="min-w-0 flex-1 truncate text-sm font-semibold">
|
||||
<SheetTitle className="min-w-0 flex-1 truncate text-lg leading-tight font-semibold">
|
||||
{t("insights.coach.historyTitle")}
|
||||
</SheetTitle>
|
||||
) : (
|
||||
<h2 className="min-w-0 flex-1 truncate text-sm font-semibold">
|
||||
<h2 className="min-w-0 flex-1 truncate text-lg leading-tight font-semibold">
|
||||
{t("insights.coach.historyTitle")}
|
||||
</h2>
|
||||
)}
|
||||
@@ -161,7 +161,7 @@ export function ConversationsPanel({
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground size-11 shrink-0 sm:size-9"
|
||||
className="text-muted-foreground hover:text-foreground size-11 shrink-0 md:size-7"
|
||||
>
|
||||
<Link
|
||||
href="/coach/plans"
|
||||
@@ -169,7 +169,7 @@ export function ConversationsPanel({
|
||||
aria-label={t("coach.plans.title")}
|
||||
title={t("coach.plans.title")}
|
||||
>
|
||||
<Target className="size-4" aria-hidden="true" />
|
||||
<Target className="size-5 md:size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
<CoachSettingsOverlay
|
||||
@@ -179,6 +179,7 @@ export function ConversationsPanel({
|
||||
if (!next) setSettingsOnData(false);
|
||||
}}
|
||||
focusData={settingsOnData}
|
||||
className="md:size-7 [&_svg]:size-5 md:[&_svg]:size-4"
|
||||
/>
|
||||
{inSheet ? (
|
||||
<SheetClose asChild>
|
||||
@@ -189,9 +190,9 @@ export function ConversationsPanel({
|
||||
data-slot="coach-panel-close"
|
||||
aria-label={t("common.close")}
|
||||
title={t("common.close")}
|
||||
className="text-muted-foreground hover:text-foreground size-11 shrink-0 sm:size-9"
|
||||
className="text-muted-foreground hover:text-foreground size-11 shrink-0 md:size-7"
|
||||
>
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
<X className="size-5 md:size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</SheetClose>
|
||||
) : null}
|
||||
|
||||
@@ -294,7 +294,7 @@ function HistoryRow({
|
||||
title={c.title}
|
||||
data-slot="coach-history-select"
|
||||
className={cn(
|
||||
"flex min-h-11 w-full min-w-0 items-center gap-1.5 rounded-lg py-2 pr-11 pl-2 text-left text-sm transition-colors",
|
||||
"flex min-h-11 w-full min-w-0 items-center gap-1.5 rounded-md py-2 pr-11 pl-2 text-left text-sm transition-colors sm:min-h-9",
|
||||
"focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none",
|
||||
// The open conversation reads like the open page in the left
|
||||
// navigation: the same primary wash, so the two rails agree.
|
||||
|
||||
@@ -27,12 +27,19 @@ import { DemoBanner } from "./demo-banner";
|
||||
import { OfflineBanner } from "./offline-banner";
|
||||
import { SharedRecordBanner } from "./shared-record-banner";
|
||||
import { SharedRecordUnavailable } from "./shared-record-unavailable";
|
||||
import { ModulePageGate } from "@/components/layout/module-page-gate";
|
||||
import {
|
||||
isModulePageOff,
|
||||
ModulePageGate,
|
||||
} from "@/components/layout/module-page-gate";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isOnboardingPathname } from "@/lib/onboarding/wizard-steps";
|
||||
import { RecordScopeHydrationGate } from "./record-scope-hydration-gate";
|
||||
import { SidebarNav } from "./sidebar-nav";
|
||||
import { TopBar } from "./top-bar";
|
||||
|
||||
/** The route that renders edge to edge inside the shell. */
|
||||
const COACH_FULL_BLEED_PATH = "/coach";
|
||||
|
||||
// v1.4.27 MB6 — `/about` joins the public-path list so the GeoLite2
|
||||
// CC BY-SA 4.0 attribution stays reachable for unauthenticated
|
||||
// visitors. The `/about` route is already registered in `proxy.ts`
|
||||
@@ -141,6 +148,17 @@ export function AuthShell({
|
||||
inSharedRecord &&
|
||||
!isDestinationInSharedRecord(pathname, sections) &&
|
||||
!isRecordSettingsPath;
|
||||
// The Coach page is the one full-bleed route: its conversations panel sits
|
||||
// flush against the right edge of the viewport and the page owns every
|
||||
// scroll area inside it. The content box drops the centred container and
|
||||
// `<main>` drops its reserved scrollbar gutter, which would otherwise paint
|
||||
// a dead strip to the right of the panel on classic-scrollbar platforms.
|
||||
// A refusal (shared record, module off) renders a card, so it keeps the
|
||||
// ordinary padded frame.
|
||||
const fullBleed =
|
||||
pathname === COACH_FULL_BLEED_PATH &&
|
||||
!outsideSharedRecord &&
|
||||
!isModulePageOff(pathname, user?.modules);
|
||||
// The whole setup flow, not only its front door: every screen lives under
|
||||
// `/onboarding/<step>` (v1.39 C2), and an exact match here put the
|
||||
// questions inside the full app chrome, sidebar and all.
|
||||
@@ -461,7 +479,10 @@ export function AuthShell({
|
||||
// window. Anchoring the scroll container closes the escape route
|
||||
// for all of them at once instead of asking each new
|
||||
// absolutely-positioned child to remember its own wrapper.
|
||||
className="relative flex-1 [scrollbar-gutter:stable] overflow-y-auto pb-[calc(4rem+env(safe-area-inset-bottom,0px))] md:pb-0"
|
||||
className={cn(
|
||||
"relative flex-1 overflow-y-auto pb-[calc(4rem+env(safe-area-inset-bottom,0px))] md:pb-0",
|
||||
fullBleed ? "flex flex-col" : "[scrollbar-gutter:stable]",
|
||||
)}
|
||||
>
|
||||
{/*
|
||||
v1.4.33 IW9 — container normalised on `max-w-screen-xl`
|
||||
@@ -479,7 +500,12 @@ export function AuthShell({
|
||||
*/}
|
||||
<div
|
||||
data-slot="main-content-wrapper"
|
||||
className="mx-auto max-w-screen-xl px-4 pt-6 pb-20 md:px-6"
|
||||
data-full-bleed={fullBleed ? "true" : undefined}
|
||||
className={
|
||||
fullBleed
|
||||
? "flex min-h-0 flex-1 flex-col"
|
||||
: "mx-auto max-w-screen-xl px-4 pt-6 pb-20 md:px-6"
|
||||
}
|
||||
>
|
||||
{outsideSharedRecord ? (
|
||||
<SharedRecordUnavailable />
|
||||
|
||||
@@ -74,14 +74,24 @@ export function moduleOwningPath(pathname: string): ModuleKey | undefined {
|
||||
* only after the account payload resolved, so the map is never read on a
|
||||
* hydration render.
|
||||
*/
|
||||
/** True when the module that owns `pathname` is switched off. */
|
||||
export function isModulePageOff(
|
||||
pathname: string,
|
||||
modules: Partial<Record<ModuleKey, boolean>> | null | undefined,
|
||||
): boolean {
|
||||
const moduleKey = moduleOwningPath(pathname);
|
||||
return moduleKey !== undefined && modules?.[moduleKey] === false;
|
||||
}
|
||||
|
||||
export function ModulePageGate({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const { user } = useAuth();
|
||||
const { t } = useTranslations();
|
||||
|
||||
const moduleKey = moduleOwningPath(pathname);
|
||||
const off = moduleKey !== undefined && user?.modules?.[moduleKey] === false;
|
||||
if (!off) return <>{children}</>;
|
||||
if (moduleKey === undefined || !isModulePageOff(pathname, user?.modules)) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const destination = NAV_DESTINATIONS.find((d) =>
|
||||
isNavDestinationActive(d.href, pathname),
|
||||
|
||||
Reference in new issue
Block a user