From 341520f98b4adb06f3fd228947b467de8ecbd07a Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 14 Oct 2026 10:45:10 +0200 Subject: [PATCH] fix(planner): give day plan rows their full width until the controls are wanted The hover controls took their room at rest, so a place's line and a note's text broke early. A place now gives way only while hovered, a note's controls fade in over the end of its text in the note's own colour, and a picked place shows none. All three controls of a place come out together: the add-booking ticket waited for a React hover state and came a beat late. A click no longer leaves them showing; keyboard focus and an open menu do. --- client/scripts/size-baseline.json | 2 +- .../Planner/DayPlanSidebar.test.tsx | 14 ++-- .../src/components/Planner/DayPlanSidebar.tsx | 25 ++++--- client/src/components/Planner/dayPlanRows.css | 69 +++++++++++++++++++ client/src/components/Planner/noteSurface.ts | 11 +++ client/src/components/Planner/planParts.tsx | 8 ++- client/src/index.css | 6 +- 7 files changed, 107 insertions(+), 28 deletions(-) create mode 100644 client/src/components/Planner/dayPlanRows.css diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json index 61a231e3d..f1a0e2a70 100644 --- a/client/scripts/size-baseline.json +++ b/client/scripts/size-baseline.json @@ -11,7 +11,7 @@ "src/components/Map/RouteCalculator.ts": 1195, "src/components/PDF/TripPDF.tsx": 1082, "src/components/Planner/DayDetailPanel.test.tsx": 2333, - "src/components/Planner/DayPlanSidebar.test.tsx": 5380, + "src/components/Planner/DayPlanSidebar.test.tsx": 5378, "src/components/Planner/DayPlanSidebar.tsx": 2973, "src/components/Planner/PlaceFormModal.test.tsx": 2864, "src/components/Planner/PlaceFormModal.tsx": 1250, diff --git a/client/src/components/Planner/DayPlanSidebar.test.tsx b/client/src/components/Planner/DayPlanSidebar.test.tsx index 54e6bc0b0..8483dcf2e 100644 --- a/client/src/components/Planner/DayPlanSidebar.test.tsx +++ b/client/src/components/Planner/DayPlanSidebar.test.tsx @@ -4015,8 +4015,7 @@ describe('DayPlanSidebar', () => { expect(document.querySelectorAll('img[src*="a.jpg"]')).toHaveLength(1) }) - it('FE-PLANNER-DAYPLAN-165: the add-booking shortcut appears while the row is hovered', async () => { - const user = userEvent.setup() + it('FE-PLANNER-DAYPLAN-165: the add-booking shortcut comes out with the row hover in CSS, like the "…"', () => { const place = buildPlace({ id: 1, name: 'Hover place' }) const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) const a = buildAssignment({ id: 11, day_id: 10, order_index: 0, place }) @@ -4024,13 +4023,12 @@ describe('DayPlanSidebar', () => { render() - const row = dragRow(screen.getByText('Hover place')) - expect(screen.queryByLabelText('Add booking')).not.toBeInTheDocument() - fireEvent.mouseEnter(row) - await user.click(screen.getByLabelText('Add booking')) + // Already in the row, hidden and inert until the row is hovered. Waiting for a + // React hover state put it on screen a beat after the arrows and the "…". + const button = screen.getByLabelText('Add booking') + expect(button).toHaveClass('dp-add-booking') + fireEvent.click(button) expect(onAddBookingToAssignment).toHaveBeenCalledWith(10, 11) - fireEvent.mouseLeave(row) - expect(screen.queryByLabelText('Add booking')).not.toBeInTheDocument() }) it('FE-PLANNER-DAYPLAN-166: the lock tooltip switches once a stop is pinned, and unlocking clears it', async () => { diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 518486da6..ac3ee34da 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -34,6 +34,7 @@ import { routeModeIcon, useRouteModeOptions } from './routeModes' import { Tooltip } from '../shared/Tooltip' import { NEUTRAL_TINT, fs } from '../shared/DialogShell' import { BarButton, MoreButton, SoftPill, TimePill, tintOf } from './planParts' +import './dayPlanRows.css' /** The round white button a row carries on its right (a route on the map, a journey's legs). */ const ROW_ROUND = 'grid flex-none place-items-center rounded-full bg-surface-card shadow-sm transition-colors' @@ -60,7 +61,7 @@ import { useDayNotes } from '../../hooks/useDayNotes' import { useExchangeRates } from '../../hooks/useExchangeRates' import { RES_ICONS, getNoteIcon } from './DayPlanSidebar.constants' import { withDayPlanDefaults } from './DayPlanSidebar.defaults' -import { noteSurface } from './noteSurface' +import { dayBodyStyle, noteSurface } from './noteSurface' import { findTodayDayId } from './today' import { markdownLinkComponents } from '../shared/markdownLink' import { RouteConnector, HotelRouteConnector } from './DayPlanSidebarRouteConnector' @@ -1282,7 +1283,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const canManageShare = can('share_manage', trip) const [lockHoverId, setLockHoverId] = useState(null) const [undoHover, setUndoHover] = useState(false) - const [hoveredAssignmentId, setHoveredAssignmentId] = useState(null) // Transit rows fold their itinerary out inline (#1065). const [expandedTransitIds, setExpandedTransitIds] = useState>(new Set()) // Deleting a note asks for confirmation first — the edit/delete icons sit close together and are @@ -1731,7 +1731,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP className="border-t border-edge-faint px-1.5 pb-1.5 pt-1.5" // The activity list — the largest region and the one behind the // densest text, so its tint is the faintest of the three. - style={{ background: dayTintBackground(dayTint, 'activity', '--day-tint-activity') ?? undefined }} + style={dayBodyStyle(dayTintBackground(dayTint, 'activity', '--day-tint-activity'))} onDragOver={e => { e.preventDefault(); const cur = dropTargetRef.current; if (draggingId && (!cur || cur.startsWith('end-'))) setDropTargetKey(`end-${day.id}`) }} onDrop={e => { e.preventDefault() @@ -1975,8 +1975,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP selectPlace() }} onContextMenu={e => ctxMenu.open(e, placeMenu())} - onMouseEnter={() => setHoveredAssignmentId(assignment.id)} - onMouseLeave={() => setHoveredAssignmentId(null)} style={{ display: 'flex', alignItems: 'center', gap: 8, paddingBlock: 6, paddingInlineEnd: 4, paddingInlineStart: gripsShown ? 2 : 8, @@ -2134,7 +2132,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP )} {/* The row's own controls, with room between them so each is its own target. */} -
+
{canEditDays &&
} - {canEditDays && onAddBookingToAssignment && (hoveredAssignmentId === assignment.id || isPlaceSelected) && ( + {canEditDays && onAddBookingToAssignment && ( @@ -2500,7 +2498,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP background: noteSkin.background, opacity: draggingId === `note-${note.id}` ? 0.4 : 1, transition: 'background 0.1s', cursor: canEditDays ? 'pointer' : 'default', userSelect: 'none', - }} + '--dp-note-tint': noteSkin.background, + } as React.CSSProperties} > {canEditDays && !dragDisabled &&
@@ -2521,17 +2520,17 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
)}
- {canEditDays &&
+ {canEditDays &&
+
-
} - {canEditDays && ( +
openEditNote(day.id, note) }, { divider: true }, { label: t('common.delete'), icon: Trash2, danger: true, onClick: () => setPendingDeleteNote({ dayId: day.id, noteId: note.id }) }, ]} /> - )} +
}
) diff --git a/client/src/components/Planner/dayPlanRows.css b/client/src/components/Planner/dayPlanRows.css new file mode 100644 index 000000000..11d205959 --- /dev/null +++ b/client/src/components/Planner/dayPlanRows.css @@ -0,0 +1,69 @@ +/* Day plan rows: the controls that only show on hover take no room at rest + either, so a row's text runs to the edge of its card. A place gives way while + they are out (its one line truncates). A note's text wraps over several + lines, so its controls fade in over the end of the text instead of + reflowing it. Hover, keyboard focus and an open menu bring them out, the + focus a click leaves on a row does not; on touch they stay in the row, like + the reorder arrows (index.css). */ +@media (hover: hover) and (pointer: fine) { + .dp-row:not(:hover):not(:focus-visible):not(:has(:focus-visible)):not(:has([data-menu-open])) .dp-row-actions { + max-width: 0; + overflow: hidden; + padding-inline-start: 0; + } + /* The fade is the note's own surface: its tint over the day's tint (a plugin + may set one) over the card, drawn as layers because a tint alone is + see-through. All come from theme variables, so it matches in light and + dark and for every note colour. */ + .dp-note-actions { + position: absolute; + inset-block: 0; + inset-inline-end: 0; + padding-inline: 32px 4px; + border-start-end-radius: 11px; + border-end-end-radius: 11px; + background: + linear-gradient(to left, var(--dp-note-tint) 60%, transparent), + linear-gradient(to left, var(--dp-day-tint, transparent) 60%, transparent), + linear-gradient(to left, var(--bg-card) 60%, transparent); + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; + } + [dir='rtl'] .dp-note-actions { + background: + linear-gradient(to right, var(--dp-note-tint) 60%, transparent), + linear-gradient(to right, var(--dp-day-tint, transparent) 60%, transparent), + linear-gradient(to right, var(--bg-card) 60%, transparent); + } + .dp-row:hover .dp-note-actions, + .dp-row:focus-visible .dp-note-actions, + .dp-row:has(:focus-visible) .dp-note-actions, + .dp-row:has([data-menu-open]) .dp-note-actions { + opacity: 1; + pointer-events: auto; + } +} + +/* The add-booking ticket beside a place's arrows and "...": out with the row's + hover in CSS like them. A React hover state re-rendered the whole day list + first, so it always came out a beat behind the others. A picked place shows + no controls of its own; on touch, with no hover, the ticket stays out. */ +.dp-add-booking { + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; +} +.dp-row:hover .dp-add-booking, +.dp-row:focus-visible .dp-add-booking, +.dp-row:has(:focus-visible) .dp-add-booking, +.dp-row:has([data-menu-open]) .dp-add-booking { + opacity: 1; + pointer-events: auto; +} +@media (hover: none) { + .dp-add-booking { + opacity: 1; + pointer-events: auto; + } +} diff --git a/client/src/components/Planner/noteSurface.ts b/client/src/components/Planner/noteSurface.ts index 97862c1ca..66e0f4127 100644 --- a/client/src/components/Planner/noteSurface.ts +++ b/client/src/components/Planner/noteSurface.ts @@ -1,3 +1,5 @@ +import type { CSSProperties } from 'react' + /** * The four surfaces a note colour produces (#1629). * @@ -34,3 +36,12 @@ export function noteSurface(color: string | null | undefined): NoteSurface { iconColor: color, } } + +/** + * A day's activity area in the plan: its tint as the background, and named as + * a variable for the note fades drawn over it (dayPlanRows.css), so a fade + * matches a day a plugin has tinted too. + */ +export function dayBodyStyle(tint: string | null | undefined): CSSProperties { + return { background: tint ?? undefined, '--dp-day-tint': tint ?? 'transparent' } as CSSProperties +} diff --git a/client/src/components/Planner/planParts.tsx b/client/src/components/Planner/planParts.tsx index a55d657c2..2179cf9e6 100644 --- a/client/src/components/Planner/planParts.tsx +++ b/client/src/components/Planner/planParts.tsx @@ -49,7 +49,9 @@ export interface MenuEntry { /** * The "…" of a row or card: the actions a right-click also offers, for the * mouse that doesn't right-click and for the keyboard. Quiet at rest, it comes - * forward with its row's hover or focus (the row carries the `group` class). + * forward with its row's hover or keyboard focus (the row carries the `group` + * class). Keyboard focus only: a row a click selected keeps the focus, and the + * button stayed out on it after the mouse had left. */ export function MoreButton({ label, items, size = 26, alwaysVisible = false, className = '' }: { label: string @@ -66,8 +68,8 @@ export function MoreButton({ label, items, size = 26, alwaysVisible = false, cla <> diff --git a/client/src/index.css b/client/src/index.css index de5eb915b..2bf9bee9b 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -253,9 +253,9 @@ html.dark .bg-slate-50\/60, html.dark [class*="bg-slate-50/"] { background-color background: none !important; padding: 1px 2px; } -/* Reveal on row hover (and on keyboard focus, so the buttons are reachable by Tab). */ -.dp-row:hover .reorder-buttons, -.dp-row:focus-within .reorder-buttons { +/* Reveal on row hover and on keyboard focus (Tab), not on the focus a click leaves on the row. */ +.dp-row:hover .reorder-buttons, .dp-row:focus-visible .reorder-buttons, +.dp-row:has(:focus-visible) .reorder-buttons { opacity: 1; pointer-events: auto; }