From 477e44ef506f190312d6fb68a5078627a4883e95 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 23 Aug 2026 21:22:14 +0200 Subject: [PATCH] fix(journey): keep the mobile timeline at the bottom of the screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2022 The story preview carried both `block` and `line-clamp-2`. Tailwind emits the display plugin after line-clamp, same specificity, so `display:block` won and `-webkit-line-clamp` — which only does anything on a -webkit-box — was inert. The whole entry text rendered, the card grew with it, and the timeline it sits in is bottom-anchored with no height of its own, so it climbed up the screen. Measured at 430x932: three words put its top edge at y=744, four hundred words at y=24, two thousand at y=-2916. That is the reporter's "at the top in one journey, in the middle in another" — it is not the install, it is the longest comment in the journey being opened. The same growth explains the other half. The timeline sits above the map and is a horizontal scroller, so once it covers the screen it takes every drag Leaflet would have got, and only the gap between it and the dock still reaches the map. Dropping the display utility fixes today's cards; capping the card height means no future one can do this again. 140px is what the older journey timeline pins its active card to, so the two look like the same product. Also lift the map attribution over the dock, which the reporter's screenshots show sitting on top of it. Both engines, since a phone meets either. --- client/src/mobile/mobile.css | 13 +++++ .../journey/MJourneyEntryCard.test.tsx | 56 +++++++++++++++++++ .../screens/journey/MJourneyEntryCard.tsx | 8 ++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 client/src/mobile/screens/journey/MJourneyEntryCard.test.tsx diff --git a/client/src/mobile/mobile.css b/client/src/mobile/mobile.css index 4b2dcf9ae..3b2ac8ee2 100644 --- a/client/src/mobile/mobile.css +++ b/client/src/mobile/mobile.css @@ -327,3 +327,16 @@ /* Zzz would sit invisible at opacity 0 without their animation — pin them visible */ .trek-z { opacity: 1; } } + +/* Map attribution clears the dock. + * + * Both engines park their attribution at the bottom of the map, and on a phone + * the map is the whole screen — so the credit line lands under the floating nav + * and reads as a broken bar (#2022's screenshots show it over the dock). Lift it + * by the same variable everything else clears the dock with. Leaflet and + * MapLibre both, because a phone meets either depending on the screen. */ +.m-root .leaflet-control-attribution, +.m-root .maplibregl-ctrl-bottom-left, +.m-root .maplibregl-ctrl-bottom-right { + bottom: var(--bottom-nav-h); +} diff --git a/client/src/mobile/screens/journey/MJourneyEntryCard.test.tsx b/client/src/mobile/screens/journey/MJourneyEntryCard.test.tsx new file mode 100644 index 000000000..1a2426172 --- /dev/null +++ b/client/src/mobile/screens/journey/MJourneyEntryCard.test.tsx @@ -0,0 +1,56 @@ +// FE-COMP-MJOURNEYCARD-001 to FE-COMP-MJOURNEYCARD-003 +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '../../../../tests/helpers/render' +import MJourneyEntryCard from './MJourneyEntryCard' +import type { JourneyEntry } from '../../../store/journeyStore' + +// #2022: a long entry comment grew the card, and with it the bottom-anchored +// carousel, until the timeline covered the map. jsdom does no layout, so these +// assert the class contract the browser cascade depends on, not pixel heights. +const LONG_STORY = Array.from({ length: 400 }, (_, i) => `wort${i % 7}`).join(' ') + +// Tailwind emits the display plugin AFTER lineClamp, so any display utility on +// the same element overrides `line-clamp-*`'s own `-webkit-box` and the clamp +// silently stops working. +const DISPLAY_UTILITIES = ['block', 'inline-block', 'flex', 'inline-flex', 'grid', 'inline-grid', 'flow-root', 'contents', 'inline'] + +const buildEntry = (overrides: Partial = {}): JourneyEntry => ({ + id: 1, + journey_id: 7, + author_id: 1, + type: 'entry', + title: 'Erster Tag', + story: LONG_STORY, + entry_date: '2026-03-03', + visibility: 'private', + sort_order: 0, + photos: [], + created_at: 0, + updated_at: 0, + ...overrides, +}) + +describe('MJourneyEntryCard', () => { + it('FE-COMP-MJOURNEYCARD-001: keeps the story preview clamp free of a competing display utility', () => { + render() + + const classes = screen.getByText(LONG_STORY).className.split(/\s+/) + expect(classes).toContain('line-clamp-2') + DISPLAY_UTILITIES.forEach(utility => expect(classes).not.toContain(utility)) + }) + + it('FE-COMP-MJOURNEYCARD-002: caps the card height so the timeline cannot grow over the map', () => { + render() + + const classes = screen.getByRole('button').className.split(/\s+/) + expect(classes.some(c => /^max-h-\[\d+px\]$/.test(c))).toBe(true) + expect(classes).toContain('overflow-hidden') + }) + + it('FE-COMP-MJOURNEYCARD-003: renders no preview for an entry without a story', () => { + render() + + expect(screen.queryByText(LONG_STORY)).not.toBeInTheDocument() + expect(screen.getByText('Erster Tag')).toBeInTheDocument() + }) +}) diff --git a/client/src/mobile/screens/journey/MJourneyEntryCard.tsx b/client/src/mobile/screens/journey/MJourneyEntryCard.tsx index 6635507b0..e12ac6e61 100644 --- a/client/src/mobile/screens/journey/MJourneyEntryCard.tsx +++ b/client/src/mobile/screens/journey/MJourneyEntryCard.tsx @@ -26,11 +26,14 @@ export default function MJourneyEntryCard({ entry, number, onClick }: MJourneyEn const storyPreview = entry.story ? stripMarkdown(entry.story) : '' const title = entry.title || (entry.type === 'checkin' ? t('journey.detail.journeyTab') : t('journey.editor.titlePlaceholder')) + // The timeline this card sits in is bottom-anchored with no height of its own, so a card + // that grows with its story climbs over the map and swallows its touches (#2022). Cap the + // card at the height the old journey timeline pins its active card to. return (