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 ? (
- +
) : subtitle && (
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 */}
-
+
{canEdit && ( -
- {([['write', t('common.edit')], ['preview', t('common.preview')]] as const).map(([tab, label]) => ( - - ))} +
+ {notesTab === 'write' && ( +
+ {MD_TOOLS.map(({ Icon, label, action }) => ( + + ))} +
+ )} +
+ {([['write', t('common.edit')], ['preview', t('common.preview')]] as const).map(([tab, label]) => ( + + ))} +
)}
{canEdit && notesTab === 'write' ? (