diff --git a/client/src/components/Planner/DayPlanSidebar.test.tsx b/client/src/components/Planner/DayPlanSidebar.test.tsx
index 828d1bffb..6da09aeee 100644
--- a/client/src/components/Planner/DayPlanSidebar.test.tsx
+++ b/client/src/components/Planner/DayPlanSidebar.test.tsx
@@ -345,7 +345,8 @@ describe('DayPlanSidebar', () => {
// Line chip + transfer summary render inline in the timeline row; the
// title uses an arrow icon, so its parts are separate text nodes.
expect(screen.getByText('U2')).toBeInTheDocument()
- expect(screen.getByText(/1 transfers/)).toBeInTheDocument()
+ // Transfer counts stay out of the compact row — the chips say it all.
+ expect(screen.queryByText(/1 transfers/)).not.toBeInTheDocument()
// Clicking the row opens the journey view — not the edit form.
await user.click(screen.getByText('Fernsehturm'))
expect(onEditTransport).not.toHaveBeenCalled()
diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx
index 02b2cad30..f78ab874e 100644
--- a/client/src/components/Planner/DayPlanSidebar.tsx
+++ b/client/src/components/Planner/DayPlanSidebar.tsx
@@ -2089,7 +2089,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
{transitMeta ? (
diff --git a/client/src/components/Planner/TransitJourneyModal.test.tsx b/client/src/components/Planner/TransitJourneyModal.test.tsx
index b94ee94fb..a746e9878 100644
--- a/client/src/components/Planner/TransitJourneyModal.test.tsx
+++ b/client/src/components/Planner/TransitJourneyModal.test.tsx
@@ -84,6 +84,18 @@ describe('TransitJourneyModal', () => {
expect(bold?.textContent).toBe('bold')
})
+ it('FE-PLANNER-TRANSITJOURNEY-007: the markdown toolbar wraps the note text', async () => {
+ const user = userEvent.setup()
+ render(
)
+ const area = screen.getByPlaceholderText(/notes/i) as HTMLTextAreaElement
+ await user.type(area, 'coffee')
+ area.setSelectionRange(0, 6)
+ await user.click(screen.getByRole('button', { name: 'Bold' }))
+ expect(area.value).toBe('**coffee**')
+ await user.click(screen.getByRole('button', { name: 'Checklist' }))
+ expect((screen.getByPlaceholderText(/notes/i) as HTMLTextAreaElement).value).toMatch(/^- \[ \] /)
+ })
+
it('FE-PLANNER-TRANSITJOURNEY-003: change route triggers onChangeRoute', async () => {
const user = userEvent.setup()
const onChangeRoute = vi.fn()
diff --git a/client/src/components/Planner/TransitJourneyModal.tsx b/client/src/components/Planner/TransitJourneyModal.tsx
index 589de151b..3295b6049 100644
--- a/client/src/components/Planner/TransitJourneyModal.tsx
+++ b/client/src/components/Planner/TransitJourneyModal.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
-import { ArrowRight, ArrowRightLeft, Clock, Footprints, Pencil, RefreshCw, TramFront, Trash2 } from 'lucide-react'
+import { ArrowRight, ArrowRightLeft, Bold, Clock, Code, Footprints, Heading2, Italic, Link2, List, ListChecks, Pencil, RefreshCw, Strikethrough, TramFront, Trash2 } from 'lucide-react'
import Modal from '../shared/Modal'
import ConfirmDialog from '../shared/ConfirmDialog'
import { useTranslation } from '../../i18n'
@@ -55,6 +55,51 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
const [saving, setSaving] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
const titleInputRef = useRef
(null)
+ const notesRef = useRef(null)
+
+ // Markdown toolbar: wrap the selection / prefix the current lines, then
+ // restore focus and a sensible cursor.
+ const applyMd = (action: { wrap?: [string, string]; linePrefix?: string }) => {
+ const el = notesRef.current
+ if (!el) return
+ const start = el.selectionStart ?? 0
+ const end = el.selectionEnd ?? 0
+ const value = notes
+ let next = value
+ let selStart = start
+ let selEnd = end
+ if (action.wrap) {
+ const [pre, post] = action.wrap
+ const selected = value.slice(start, end)
+ next = value.slice(0, start) + pre + selected + post + value.slice(end)
+ selStart = start + pre.length
+ selEnd = selStart + selected.length
+ } else if (action.linePrefix) {
+ const prefix = action.linePrefix
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1
+ const block = value.slice(lineStart, end)
+ const prefixed = block.split('\n').map(l => prefix + l).join('\n')
+ next = value.slice(0, lineStart) + prefixed + value.slice(end)
+ selStart = start + prefix.length
+ selEnd = end + (prefixed.length - block.length)
+ }
+ setNotes(next)
+ requestAnimationFrame(() => {
+ el.focus()
+ el.setSelectionRange(selStart, selEnd)
+ })
+ }
+
+ const MD_TOOLS: { Icon: typeof Bold; label: string; action: { wrap?: [string, string]; linePrefix?: string } }[] = [
+ { Icon: Bold, label: 'Bold', action: { wrap: ['**', '**'] } },
+ { Icon: Italic, label: 'Italic', action: { wrap: ['*', '*'] } },
+ { Icon: Strikethrough, label: 'Strikethrough', action: { wrap: ['~~', '~~'] } },
+ { Icon: Heading2, label: 'Heading', action: { linePrefix: '## ' } },
+ { Icon: List, label: 'List', action: { linePrefix: '- ' } },
+ { Icon: ListChecks, label: 'Checklist', action: { linePrefix: '- [ ] ' } },
+ { Icon: Link2, label: 'Link', action: { wrap: ['[', '](https://)'] } },
+ { Icon: Code, label: 'Code', action: { wrap: ['`', '`'] } },
+ ]
useEffect(() => {
setTitle(res.title || '')
@@ -241,22 +286,38 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
{/* notes — full width, markdown */}
-
+
{t('reservations.notes')}
{canEdit && (
-
- {([['write', t('common.edit')], ['preview', t('common.preview')]] as const).map(([tab, label]) => (
-
setNotesTab(tab)}
- className={notesTab === tab ? 'bg-surface-card text-content' : 'text-content-muted'}
- style={{ padding: '4px 12px', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 500, borderRadius: 6, border: 0, cursor: 'pointer', fontFamily: 'inherit', background: notesTab === tab ? undefined : 'transparent' }}>
- {label}
-
- ))}
+
+ {notesTab === 'write' && (
+
+ {MD_TOOLS.map(({ Icon, label, action }) => (
+ applyMd(action)} title={label} aria-label={label}
+ className="text-content-muted"
+ style={{ width: 26, height: 24, display: 'grid', placeItems: 'center', borderRadius: 6, border: 0, background: 'transparent', cursor: 'pointer' }}
+ onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-card)' }}
+ onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}>
+
+
+ ))}
+
+ )}
+
+ {([['write', t('common.edit')], ['preview', t('common.preview')]] as const).map(([tab, label]) => (
+ setNotesTab(tab)}
+ className={notesTab === tab ? 'bg-surface-card text-content' : 'text-content-muted'}
+ style={{ padding: '4px 12px', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 500, borderRadius: 6, border: 0, cursor: 'pointer', fontFamily: 'inherit', background: notesTab === tab ? undefined : 'transparent' }}>
+ {label}
+
+ ))}
+
)}
{canEdit && notesTab === 'write' ? (
- {/* time + depart/arrive */}
-
-
-
setArriveBy(false)} style={segBtn(!arriveBy)}>{t('transit.depart')}
-
setArriveBy(true)} style={segBtn(arriveBy)}>{t('transit.arrive')}
+ {/* search options — one calm card: when + how on top, modes + go below */}
+
+
+
+
+ setArriveBy(false)} style={segBtn(!arriveBy)}>{t('transit.depart')}
+ setArriveBy(true)} style={segBtn(arriveBy)}>{t('transit.arrive')}
+
+
+
+
+ {day.date && (
+
+
+ {new Date(day.date + 'T00:00:00Z').toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}
+
+ )}
+
+
+ setPref('best')} style={segBtn(pref === 'best')}>{t('transit.pref.best')}
+ setPref('transfers')} style={segBtn(pref === 'transfers')}>{t('transit.pref.transfers')}
+ setPref('walking')} style={segBtn(pref === 'walking')}>{t('transit.pref.walking')}
+
-
-
-
- {day.date && (
-
-
- {new Date(day.date + 'T00:00:00Z').toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' })}
-
- )}
-
- {/* mode chips */}
-
- {MODE_GROUPS.map(m => {
- const active = activeModes.has(m.key)
- return (
- toggleMode(m.key)}
- style={{
- display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 11px', borderRadius: 99,
- border: '1.5px solid', fontSize: 'calc(11.5px * var(--fs-scale-caption, 1))', fontWeight: 600,
- cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.12s',
- background: active ? 'var(--text-primary)' : 'transparent',
- borderColor: active ? 'var(--text-primary)' : 'var(--border-primary)',
- color: active ? 'var(--bg-primary)' : 'var(--text-muted)',
- }}>
-
- {t(m.labelKey)}
-
- )
- })}
-
+
- {/* preference + search */}
-
-
-
setPref('best')} style={segBtn(pref === 'best')}>{t('transit.pref.best')}
-
setPref('transfers')} style={segBtn(pref === 'transfers')}>{t('transit.pref.transfers')}
-
setPref('walking')} style={segBtn(pref === 'walking')}>{t('transit.pref.walking')}
+
+
+ {MODE_GROUPS.map(m => {
+ const active = activeModes.has(m.key)
+ return (
+ toggleMode(m.key)}
+ className={active ? 'bg-surface-card text-content' : 'text-content-faint'}
+ style={{
+ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 11px', borderRadius: 99,
+ border: '1px solid', fontSize: 'calc(11.5px * var(--fs-scale-caption, 1))', fontWeight: 500,
+ cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.12s',
+ background: active ? undefined : 'transparent',
+ borderColor: active ? 'var(--border-primary)' : 'transparent',
+ boxShadow: active ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
+ opacity: active ? 1 : 0.75,
+ }}>
+
+ {t(m.labelKey)}
+
+ )
+ })}
+
+
+
+ {loading ? t('transit.searching') : t('transit.search')}
+
-
-
- {loading ? t('transit.searching') : t('transit.search')}
-
{/* results */}