mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
merge: coach card redesign — wider rail, symmetric dividers, FAB inset
This commit is contained in:
commit
4a10505500
7 files changed
+77
-33
No files matched your search
@@ -61,7 +61,14 @@ export default function CoachPage() {
|
||||
return (
|
||||
<div
|
||||
data-slot="coach-page"
|
||||
className="flex h-[calc(100dvh-13rem)] min-h-[28rem] flex-col overflow-hidden rounded-2xl border"
|
||||
// v1.18.1 (W-COACH-UI C1/C3) — the card was capped at
|
||||
// `100dvh-13rem`, leaving a band of wasted space below it on a
|
||||
// standalone route that no longer carries the Insights tab strip.
|
||||
// Claim the page height down to a comfortable bottom gutter so the
|
||||
// thread + rail fill the viewport, and lift the card with a soft
|
||||
// shadow so it reads as one contained surface rather than a thin
|
||||
// outline.
|
||||
className="bg-card flex h-[calc(100dvh-8rem)] min-h-[32rem] flex-col overflow-hidden rounded-2xl border shadow-sm"
|
||||
>
|
||||
<CoachConversation
|
||||
surface="page"
|
||||
|
||||
@@ -58,13 +58,16 @@ describe("<LayoutCoachFab>", () => {
|
||||
// Fixed bottom-right, above the mobile bottom-nav band. The plain
|
||||
// desktop offset kicks in at `md:` — the same breakpoint where the
|
||||
// bottom-nav hides (`md:hidden`) — so the FAB never floats mid-air
|
||||
// in the 768-1023px band. The right inset mirrors the desktop
|
||||
// bottom inset so the corner gap is even.
|
||||
// in the 768-1023px band. v1.18.1 (C5) — the corner inset is
|
||||
// symmetric: right equals bottom at each breakpoint (`right-6`
|
||||
// mobile-band / `md:right-8` desktop mirrors `md:bottom-8`).
|
||||
expect(html).toContain("fixed right-6");
|
||||
expect(html).toContain("md:right-8");
|
||||
expect(html).toContain(
|
||||
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)]",
|
||||
);
|
||||
expect(html).toContain("md:bottom-6");
|
||||
expect(html).toContain("md:bottom-8");
|
||||
expect(html).not.toContain("md:bottom-6");
|
||||
expect(html).not.toContain("lg:bottom-6");
|
||||
});
|
||||
|
||||
|
||||
@@ -291,12 +291,23 @@ export function CoachConversation({
|
||||
>
|
||||
<header
|
||||
data-slot="coach-conversation-header"
|
||||
className="border-border/70 flex flex-row items-center gap-2 border-b p-3 sm:gap-3 sm:p-4"
|
||||
// v1.18.1 (W-COACH-UI C2/C4) — the header used to crowd a bare
|
||||
// leading ghost-icon flush against the avatar (the "minimal"
|
||||
// element the maintainer disliked). The surface-toggle now sits
|
||||
// in its own leading cluster separated by a hairline divider, the
|
||||
// avatar steps up to `size-9`, and the band locks to `h-14` so
|
||||
// its bottom border shares the horizontal line with the body's
|
||||
// rail-tray strip + history-rail heading one row below.
|
||||
className="border-border/70 flex h-14 shrink-0 flex-row items-center gap-2.5 border-b px-3 sm:px-4"
|
||||
>
|
||||
{leadingHeaderActions}
|
||||
{leadingHeaderActions ? (
|
||||
<div className="border-border/60 -ml-1 flex items-center gap-1 pr-1 sm:border-r sm:pr-2.5">
|
||||
{leadingHeaderActions}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-dracula-purple to-dracula-pink flex size-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br shadow-sm"
|
||||
>
|
||||
<Sparkles className="text-background size-4" />
|
||||
</div>
|
||||
|
||||
@@ -53,9 +53,13 @@ export function CoachDrawerBody({
|
||||
return (
|
||||
<div
|
||||
data-slot="coach-drawer-body"
|
||||
// v1.18.1 (W-COACH-UI C1/C3) — the inline history rail was a
|
||||
// tight 260 px gutter; widen it to 300 px (lg) / 340 px (xl) so
|
||||
// the conversation list breathes and the centre thread keeps the
|
||||
// remaining width. Without a rail the body is a single column.
|
||||
className={
|
||||
historyRail
|
||||
? "grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[260px_1fr]"
|
||||
? "grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[300px_1fr] xl:grid-cols-[340px_1fr]"
|
||||
: "grid min-h-0 flex-1 grid-cols-1"
|
||||
}
|
||||
>
|
||||
@@ -65,7 +69,13 @@ export function CoachDrawerBody({
|
||||
data-slot="coach-drawer-history"
|
||||
className="border-border/70 hidden h-full min-h-0 border-r lg:flex lg:flex-col"
|
||||
>
|
||||
<h2 className="border-border/70 text-muted-foreground border-b p-3 text-[11px] font-medium tracking-wide uppercase">
|
||||
{/* v1.18.1 (W-COACH-UI C4) — the rail heading and the centre
|
||||
column's rail-tray strip share one fixed `h-14` header band
|
||||
with identical `border-b`, so the two dividers land on the
|
||||
same horizontal line across the columns (the asymmetry the
|
||||
maintainer flagged: a `p-3` text heading vs a `py-2` button
|
||||
strip put the lines at different heights). */}
|
||||
<h2 className="border-border/70 text-muted-foreground flex h-14 shrink-0 items-center border-b px-4 text-xs font-medium tracking-wide uppercase">
|
||||
{t("insights.coach.historyTitle")}
|
||||
</h2>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">{historyRail}</div>
|
||||
@@ -80,10 +90,14 @@ export function CoachDrawerBody({
|
||||
{/* v1.4.27 R3d MB2 — rail-tray triggers lifted out of the
|
||||
absolute overlay into a sub-header strip so the buttons
|
||||
sit at a 44 px tap target and never overlay the first
|
||||
message bubble. */}
|
||||
message bubble.
|
||||
v1.18.1 (W-COACH-UI C4) — fixed `h-14` so the strip's
|
||||
bottom border aligns with the history rail heading divider
|
||||
in the adjacent column (one continuous line, no restless
|
||||
two-height split). */}
|
||||
<div
|
||||
data-slot="coach-drawer-rail-tray-strip"
|
||||
className="border-border/70 flex items-center justify-between gap-2 border-b px-3 py-2"
|
||||
className="border-border/70 flex h-14 shrink-0 items-center justify-between gap-2 border-b px-3"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -67,7 +67,9 @@ export function HistoryRail({
|
||||
return (
|
||||
<div
|
||||
data-slot="coach-history-rail"
|
||||
className={cn("flex h-full min-h-0 flex-col gap-2 p-3", className)}
|
||||
// v1.18.1 (W-COACH-UI C1/C3) — a touch more horizontal padding so
|
||||
// the list rows clear the rail edge and the search field breathes.
|
||||
className={cn("flex h-full min-h-0 flex-col gap-2.5 p-3.5", className)}
|
||||
>
|
||||
{/* v1.4.33 — promote the rail label from a `<span>` to a real
|
||||
`<h3>` so the drawer carries a semantic outline on desktop
|
||||
@@ -78,7 +80,7 @@ export function HistoryRail({
|
||||
(the rail still has to stand alone on desktop). */}
|
||||
<h3
|
||||
data-slot="coach-history-rail-heading"
|
||||
className="text-muted-foreground flex items-center gap-1.5 text-[11px] font-medium tracking-wide uppercase"
|
||||
className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium tracking-wide uppercase"
|
||||
>
|
||||
<MessagesSquare
|
||||
className="text-muted-foreground size-3.5"
|
||||
@@ -97,7 +99,7 @@ export function HistoryRail({
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder={t("insights.coach.historySearchPlaceholder")}
|
||||
data-slot="coach-history-search"
|
||||
className="h-8 pl-7 text-xs"
|
||||
className="h-9 pl-7 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -107,14 +109,14 @@ export function HistoryRail({
|
||||
{isLoading && conversations.length === 0 ? (
|
||||
<p
|
||||
data-slot="coach-history-loading"
|
||||
className="text-muted-foreground px-2 py-3 text-xs"
|
||||
className="text-muted-foreground px-2 py-3 text-sm"
|
||||
>
|
||||
{t("common.loading")}
|
||||
</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p
|
||||
data-slot="coach-history-empty"
|
||||
className="text-muted-foreground px-2 py-3 text-xs leading-relaxed"
|
||||
className="text-muted-foreground px-2 py-3 text-sm leading-relaxed"
|
||||
>
|
||||
{t("insights.coach.historyEmpty")}
|
||||
</p>
|
||||
@@ -128,8 +130,8 @@ export function HistoryRail({
|
||||
data-slot="coach-history-item"
|
||||
data-active={isActive ? "true" : undefined}
|
||||
className={cn(
|
||||
"group flex items-center gap-1.5 rounded-md px-2 py-1.5",
|
||||
"text-xs transition-colors",
|
||||
"group flex items-center gap-1.5 rounded-lg px-2.5 py-2",
|
||||
"text-sm transition-colors",
|
||||
isActive
|
||||
? "bg-dracula-purple/15 text-foreground"
|
||||
: "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
|
||||
@@ -142,7 +144,7 @@ export function HistoryRail({
|
||||
data-slot="coach-history-select"
|
||||
>
|
||||
<span className="block truncate font-medium">{c.title}</span>
|
||||
<span className="text-muted-foreground block text-[10px]">
|
||||
<span className="text-muted-foreground block text-[11px]">
|
||||
{formatRelativeTime(c.updatedAt, t)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -105,7 +105,7 @@ export function SourcesRail({ className }: SourcesRailProps) {
|
||||
// column width: the switch rows used to push past the 280/320 px
|
||||
// container and paint a horizontal scrollbar under the rail.
|
||||
className={cn(
|
||||
"flex h-full min-h-0 min-w-0 flex-col gap-3 overflow-x-hidden p-3",
|
||||
"flex h-full min-h-0 min-w-0 flex-col gap-3 overflow-x-hidden p-3.5",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -113,8 +113,10 @@ export function SourcesRail({ className }: SourcesRailProps) {
|
||||
data-slot="coach-sources-rail-heading"
|
||||
// No horizontal inset — the heading sits flush with the row
|
||||
// boxes below it (headings and their content share one left
|
||||
// edge across the Coach surfaces).
|
||||
className="text-muted-foreground flex items-center gap-1.5 text-[11px] font-medium tracking-wide uppercase"
|
||||
// edge across the Coach surfaces). v1.18.1 (W-COACH-UI C1/C4) —
|
||||
// typography matched to the history-rail heading so the two rails
|
||||
// read as one family.
|
||||
className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium tracking-wide uppercase"
|
||||
>
|
||||
<Eye className="text-muted-foreground size-3.5" aria-hidden="true" />
|
||||
{t("insights.coach.sourcesTitle")}
|
||||
@@ -126,7 +128,7 @@ export function SourcesRail({ className }: SourcesRailProps) {
|
||||
<div data-slot="coach-sources-window" className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="coach-sources-window-select"
|
||||
className="text-muted-foreground text-[10px] font-medium tracking-wide uppercase"
|
||||
className="text-muted-foreground text-[11px] font-medium tracking-wide uppercase"
|
||||
>
|
||||
{t("insights.coach.windowLabel")}
|
||||
</label>
|
||||
@@ -139,13 +141,13 @@ export function SourcesRail({ className }: SourcesRailProps) {
|
||||
id="coach-sources-window-select"
|
||||
data-slot="coach-sources-window-trigger"
|
||||
size="default"
|
||||
className="h-9 text-xs"
|
||||
className="h-9 text-sm"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{WINDOW_OPTIONS.map((w) => (
|
||||
<SelectItem key={w} value={w} className="text-xs">
|
||||
<SelectItem key={w} value={w} className="text-sm">
|
||||
{t(`insights.coach.window.${w}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -172,19 +174,19 @@ export function SourcesRail({ className }: SourcesRailProps) {
|
||||
data-active={checked ? "true" : "false"}
|
||||
className={cn(
|
||||
"border-border/60 bg-muted/30 flex items-center gap-2.5",
|
||||
"rounded-md border px-2.5",
|
||||
"min-h-11 py-1.5",
|
||||
"rounded-lg border px-3",
|
||||
"min-h-11 py-2",
|
||||
!checked && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<label
|
||||
htmlFor={switchId}
|
||||
className="text-foreground cursor-pointer text-xs font-medium"
|
||||
className="text-foreground cursor-pointer text-sm font-medium"
|
||||
>
|
||||
{t(`insights.coach.cluster.${cluster}.label`)}
|
||||
</label>
|
||||
<span className="text-muted-foreground text-[10px] leading-snug">
|
||||
<span className="text-muted-foreground text-[11px] leading-snug">
|
||||
{t("insights.coach.sourcesMemberCount", {
|
||||
count: memberCount,
|
||||
})}
|
||||
|
||||
@@ -157,10 +157,15 @@ export function LayoutCoachFab() {
|
||||
// safe-area inset on mobile; plain bottom offset once the
|
||||
// bottom-nav hides (`md:hidden` on the nav, so `md:` here —
|
||||
// not `lg:` — keeps the FAB from floating mid-air 768-1023px).
|
||||
// The right inset mirrors the desktop bottom inset so the
|
||||
// corner gap is even.
|
||||
"fixed right-6 z-40 size-14 rounded-full shadow-lg",
|
||||
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] md:bottom-6",
|
||||
// v1.18.1 (W-COACH-UI C5) — the corner inset is now symmetric:
|
||||
// the right inset equals the bottom inset at each breakpoint
|
||||
// (`right-6`/`bottom-6` mobile-band, `md:right-8`/`md:bottom-8`
|
||||
// desktop). The FAB was hugging the right edge (`right-6`)
|
||||
// while the desktop bottom sat at the same `6`, but it read as
|
||||
// off-centre against the larger mobile bottom band; pulling it
|
||||
// a touch further left + matching both offsets evens the gap.
|
||||
"fixed right-6 z-40 size-14 rounded-full shadow-lg md:right-8",
|
||||
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] md:bottom-8",
|
||||
// Dark glyph on the purple/pink gradient — white sat at
|
||||
// ≈2.3:1 against the gradient midpoint; the background token
|
||||
// reads ≈6.5:1.
|
||||
|
||||
Reference in new issue
Block a user