diff --git a/client/src/components/Collections/CollectionPlaceDetail.tsx b/client/src/components/Collections/CollectionPlaceDetail.tsx index 42a7991da..62df37d91 100644 --- a/client/src/components/Collections/CollectionPlaceDetail.tsx +++ b/client/src/components/Collections/CollectionPlaceDetail.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkBreaks from 'remark-breaks' +import { markdownLinkComponents } from '../shared/markdownLink' import { X, Pencil, Copy, Trash2, MapPin, Link2, Plus, ExternalLink, Check, Tag, Tags, Camera, Loader2 } from 'lucide-react' import type { CollectionPlace, CollectionStatus, CollectionLink, CollectionLabel } from '@trek/shared' import type { Category, TranslationFn } from '../../types' @@ -306,7 +307,7 @@ export default function CollectionPlaceDetail({ )} {place.description && (
- {place.description} + {place.description}
)} {place.links && place.links.length > 0 && ( diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index f8fde0e19..789ee46c0 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -333,10 +333,14 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca if (item.type === 'note') { const note = item.data + // A coloured note carries its colour into print the same way a + // reservation does (#1629) — the rule and the spine, not a fill, + // because a tint costs ink and reads as grey on a mono printer. + const noteColor = note.color || '' return ` -
-
- ${noteIconSvg(note.icon)} +
+
+ ${noteIconSvg(note.icon)}
${escHtml(note.text)}
${note.time ? `
${escHtml(note.time)}
` : ''} diff --git a/client/src/components/Planner/DayPlanSidebar.test.tsx b/client/src/components/Planner/DayPlanSidebar.test.tsx index c23e760a4..c2ce18028 100644 --- a/client/src/components/Planner/DayPlanSidebar.test.tsx +++ b/client/src/components/Planner/DayPlanSidebar.test.tsx @@ -1198,6 +1198,48 @@ describe('DayPlanSidebar', () => { expect(mockDayNotesState.saveNote).toHaveBeenCalledWith(10) }) + // ── Note colours and formatting (#1629) ─────────────────────────────── + + it('FE-PLANNER-DAYPLAN-192: the note dialog offers the palette and reports the pick', async () => { + const user = userEvent.setup() + const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) + mockDayNotesState.noteUi = { + '10': { mode: 'add', text: 'Passport', time: '', icon: 'StickyNote', color: null }, + } + render() + + // Exact, not /red/i: "Ordered list" in the formatting bar matches that too. + await user.click(screen.getByRole('button', { name: 'Red' })) + + // The dialog owns no state of its own — it reports upwards, like every other field. + expect(mockDayNotesState.setNoteUi).toHaveBeenCalled() + }) + + it('FE-PLANNER-DAYPLAN-193: a note card is tinted by its colour and renders its body', () => { + const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) + mockDayNotesState.dayNotes = { + '10': [{ id: 1, day_id: 10, text: 'Ferry', time: 'book **early**', icon: 'StickyNote', color: '#dc2626', sort_order: 0 }], + } + render() + + // Markdown, not the asterisks someone typed with the formatting bar. + expect(screen.getByText('early').tagName).toBe('STRONG') + // jsdom normalises the hex inside color-mix() to rgb(). + const card = screen.getByText('Ferry').closest('.dp-row') as HTMLElement + expect(card.style.background).toContain('220, 38, 38') + }) + + it('FE-PLANNER-DAYPLAN-194: a note without a colour keeps the neutral card', () => { + const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' }) + mockDayNotesState.dayNotes = { + '10': [{ id: 2, day_id: 10, text: 'Lunch', time: '', icon: 'StickyNote', color: null, sort_order: 0 }], + } + render() + + const card = screen.getByText('Lunch').closest('.dp-row') as HTMLElement + expect(card.style.background).toContain('--bg-hover') + }) + // ── Note modal edit mode title ──────────────────────────────────────── it('FE-PLANNER-DAYPLAN-061: note modal shows Edit title in edit mode', () => { diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 06b6d137b..d5da0ec50 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -12,6 +12,7 @@ import ConfirmDialog from '../shared/ConfirmDialog' import { useContextMenu, ContextMenu } from '../shared/ContextMenu' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' +import remarkBreaks from 'remark-breaks' import WeatherWidget from '../Weather/WeatherWidget' import { useToast } from '../shared/Toast' import { getCategoryIcon } from '../shared/categoryIcons' @@ -33,6 +34,8 @@ import { formatDate, formatTime, dayTotalCost, formatMoneySum, splitReservationD import { useDayNotes } from '../../hooks/useDayNotes' import { useExchangeRates } from '../../hooks/useExchangeRates' import { RES_ICONS, getNoteIcon } from './DayPlanSidebar.constants' +import { noteSurface } from './noteSurface' +import { markdownLinkComponents } from '../shared/markdownLink' import { RouteConnector, HotelRouteConnector } from './DayPlanSidebarRouteConnector' import { usePluginDaySchedule, usePluginDayTints, dayTintBackground, dayTinted, PluginDayScheduleRow, formatScheduleMinutes } from '../Plugins/PluginDaySchedule' import { MobileAddPlaceButton } from './DayPlanSidebarMobileAddPlaceButton' @@ -1374,7 +1377,10 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const hasWeather = !!(day.date && anyGeoPlace && wLat != null && wLng != null) return (
-
+ {/* lineHeight 1, or the digit rides the line box's leading and + sits above centre on the plain badge — visible as soon as + there is no weather block under it to distract from it. */} +
{index + 1}
{hasWeather && ( @@ -2178,6 +2187,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP // Notizkarte const note = item.data const NoteIcon = getNoteIcon(note.icon) + const noteSkin = noteSurface(note.color) const noteIdx = idx return ( @@ -2241,13 +2251,14 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP if (editBtns) editBtns.style.opacity = '0' }} style={{ + position: 'relative', display: 'flex', alignItems: 'center', gap: 8, padding: '7px 8px 7px 2px', margin: '1px 8px', borderRadius: 6, - border: '1px solid var(--border-faint)', + border: `1px solid ${noteSkin.border}`, borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined, - background: 'var(--bg-hover)', + background: noteSkin.background, opacity: draggingId === `note-${note.id}` ? 0.4 : 1, transition: 'background 0.1s', cursor: 'grab', userSelect: 'none', }} @@ -2255,25 +2266,39 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP {canEditDays && !dragDisabled &&
} -
- +
+
{note.text} {note.time && ( -
{note.time}
+
+ {/* A link in a note goes to its own tab, and remarkBreaks + keeps a single newline a line break — people write notes + as lines, not as Markdown paragraphs. */} + {note.time} +
)}
- {canEditDays &&
- - -
} - {canEditDays &&
- - -
} + {/* Stacked in a capsule of their own, set off from the text: + a note card is as tall as its body, so the height is free + while the width next to the text is not, and a 10px icon + in a 14px box was barely a target. */} + {/* Floated over the text rather than sharing the row with + it: a note is mostly text, and reserving a column for + two buttons that are invisible until you hover costs + every note a shorter line for the whole time you are + not hovering. */} + {canEditDays &&
+ + +
} + {canEditDays &&
+ + +
}
) diff --git a/client/src/components/Planner/DayPlanSidebarNoteModal.tsx b/client/src/components/Planner/DayPlanSidebarNoteModal.tsx index d244ae8b1..7a87b2a44 100644 --- a/client/src/components/Planner/DayPlanSidebarNoteModal.tsx +++ b/client/src/components/Planner/DayPlanSidebarNoteModal.tsx @@ -1,82 +1,188 @@ +import { useRef } from 'react' import { createPortal } from 'react-dom' -import { NOTE_ICONS } from './DayPlanSidebar.constants' +import Markdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import remarkBreaks from 'remark-breaks' +import { NOTE_ICONS, getNoteIcon } from './DayPlanSidebar.constants' +import NoteFormatToolbar from '../shared/NoteFormatToolbar' +import NoteColorPicker from '../shared/NoteColorPicker' +import { noteSurface } from './noteSurface' +import { markdownLinkComponents } from '../shared/markdownLink' interface NoteModalUi { mode: 'add' | 'edit' icon: string text: string time: string + color?: string | null } +type NoteUiMap = Record + interface DayPlanSidebarNoteModalProps { - noteUi: Record - setNoteUi: (updater: (prev: any) => any) => void + noteUi: NoteUiMap + setNoteUi: (updater: (prev: NoteUiMap) => NoteUiMap) => void noteInputRef: React.RefObject cancelNote: (dayId: number) => void saveNote: (dayId: number) => void - t: (key: string, params?: Record) => string + t: (key: string, params?: Record) => string } +const BODY_MAX = 2000 + +/** + * Add/edit dialog for a day note (#1629). + * + * Two columns on anything wider than a phone: how the note looks on the left + * (icon, colour, and a preview of the card it will become), what it says on the + * right. The preview is the reason the colour picker is worth having — a swatch + * row tells you nothing about what a tinted card looks like next to a place. + */ export function DayPlanSidebarNoteModal({ noteUi, setNoteUi, noteInputRef, cancelNote, saveNote, t }: DayPlanSidebarNoteModalProps) { return ( <> {Object.entries(noteUi).map(([dayId, ui]) => ui && createPortal( -
cancelNote(Number(dayId))}> -
e.stopPropagation()}> -
- {ui.mode === 'add' ? t('dayplan.noteAdd') : t('dayplan.noteEdit')} -
- {/* Icon-Auswahl */} -
- {NOTE_ICONS.map(({ id, Icon }) => ( - - ))} -
- setNoteUi(prev => ({ ...prev, [dayId]: { ...prev[dayId], text: e.target.value } }))} - onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); saveNote(Number(dayId)) } if (e.key === 'Escape') cancelNote(Number(dayId)) }} - placeholder={t('dayplan.noteTitle') + ' *'} - required - className="text-content" - style={{ fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500, border: `1px solid ${!ui.text?.trim() ? 'var(--border-primary)' : 'var(--border-primary)'}`, borderRadius: 8, padding: '8px 10px', fontFamily: 'inherit', outline: 'none', width: '100%', boxSizing: 'border-box' }} - /> -