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 (