diff --git a/Summarize.md b/Summarize.md index 5b0bd20..f86546b 100644 --- a/Summarize.md +++ b/Summarize.md @@ -6,6 +6,28 @@ --- +### Aenderungen 2026-03-13 (15): Experten-Profil – BulletEditor Fett/Kursiv/Unterstrichen + Aufgaben-Anzeige + +#### Frontend +- `profile/sections/ProjectModal.tsx` — BulletEditor: Inline-Formatierungen + - Neue Hilfsfunktion `blWrapFormat()` auf Modul-Ebene: Wraps/Unwraps markierten Text mit Marker-Paar; kein Text markiert → Marker-Paar einsetzen (Cursor dazwischen); Text bereits gewrapped → aushakenlen + - Toolbar: Neuer Button "B" (Fett, `**text**`), "I" (Kursiv, `*text*`), "U" (Unterstrichen, `__text__`) + - Keyboard-Shortcuts: Strg+B, Strg+I, Strg+U (Mac: Cmd+B/I/U) +- `profile/sections/ProjectsSection.tsx` — Aufgaben-Anzeige mit Markdown-Renderer + - Neue Funktion `renderInline()`: wandelt **bold**, *italic*, __underline__ Marker in React-Elemente um (regex-basiert, kein XSS, kein dangerouslySetInnerHTML) + - Neue Komponente ``: Rendert Aufgaben-Text Zeile fuer Zeile mit Bullet-Punkt-Layout, nummerierter Liste, Einrueckung und Inline-Formatierung + - Projekt-Listenelement: zeigt Aufgaben-Text unterhalb von Taetikeit/Firma an (separator border-top) — nur wenn Aufgaben vorhanden + - Neues Layout `entryItemExpanded + entryItemRow` (column statt row wenn Aufgaben vorhanden) +- `profile/ExpertProfileTab.module.css` — Neue Klassen: `.bulletBtnBold`, `.bulletBtnItalic`, `.bulletBtnUnderline`; `.entryItemExpanded`, `.entryItemRow`, `.entryTasks`; `.richText`, `.richTextLine`, `.richTextBullet`, `.richTextNum`, `.richTextBlank` + +#### TypeScript +- `npx tsc --noEmit` in packages/frontend: 0 Fehler + +#### Deployment-Hinweis (Schritt 15) +- Rebuild + Restart: nur frontend + +--- + ### Aenderungen 2026-03-13 (14): Experten-Profil – BulletEditor mit nummerierter Liste + Tab-Einrueckung #### Frontend diff --git a/packages/frontend/src/profile/ExpertProfileTab.module.css b/packages/frontend/src/profile/ExpertProfileTab.module.css index f2263ea..f286aed 100644 --- a/packages/frontend/src/profile/ExpertProfileTab.module.css +++ b/packages/frontend/src/profile/ExpertProfileTab.module.css @@ -497,6 +497,77 @@ white-space: nowrap; } +/* B / I / U formatting buttons */ +.bulletBtnBold { + font-weight: 700; + font-size: 0.875rem; + min-width: 1.75rem; + text-align: center; +} + +.bulletBtnItalic { + font-style: italic; + font-size: 0.875rem; + min-width: 1.75rem; + text-align: center; +} + +.bulletBtnUnderline { + text-decoration: underline; + font-size: 0.875rem; + min-width: 1.75rem; + text-align: center; +} + +/* === Project list item with expanded tasks === */ +.entryItemExpanded { + flex-direction: column !important; + align-items: stretch !important; + gap: 0.5rem; +} + +.entryItemRow { + display: flex; + align-items: center; + justify-content: space-between; +} + +.entryTasks { + padding-top: 0.5rem; + border-top: 1px solid var(--color-border); + font-size: 0.875rem; + color: var(--color-text); + line-height: 1.6; +} + +/* === RichText renderer === */ +.richText { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.richTextLine { + display: flex; + gap: 0.375rem; +} + +.richTextBullet { + flex-shrink: 0; + color: var(--color-text-muted); +} + +.richTextNum { + flex-shrink: 0; + min-width: 1.375rem; + text-align: right; + color: var(--color-text-muted); +} + +.richTextBlank { + height: 0.375rem; +} + .checkboxRow { display: flex; align-items: center; diff --git a/packages/frontend/src/profile/sections/ProjectModal.tsx b/packages/frontend/src/profile/sections/ProjectModal.tsx index c0ac9f5..c10d286 100644 --- a/packages/frontend/src/profile/sections/ProjectModal.tsx +++ b/packages/frontend/src/profile/sections/ProjectModal.tsx @@ -36,6 +36,49 @@ const YEARS = Array.from({ length: 40 }, (_, i) => currentYear - i); // ─── BulletEditor helpers (module-level, pure) ─────────────────────────────── +/** + * Wrap or unwrap the current selection with an inline formatting marker. + * - With selection: wraps/unwraps (toggle) + * - Without selection: inserts marker pair with cursor inside + */ +function blWrapFormat( + ta: HTMLTextAreaElement, + v: string, + marker: string, + onChange: (val: string) => void, +): void { + const start = ta.selectionStart; + const end = ta.selectionEnd; + const mLen = marker.length; + const selected = v.substring(start, end); + + if (selected.startsWith(marker) && selected.endsWith(marker) && selected.length > mLen * 2) { + // Already wrapped → unwrap + const inner = selected.substring(mLen, selected.length - mLen); + onChange(v.substring(0, start) + inner + v.substring(end)); + requestAnimationFrame(() => { + ta.selectionStart = start; + ta.selectionEnd = start + inner.length; + ta.focus(); + }); + } else if (selected.length > 0) { + // Wrap selection + onChange(v.substring(0, start) + marker + selected + marker + v.substring(end)); + requestAnimationFrame(() => { + ta.selectionStart = start + mLen; + ta.selectionEnd = end + mLen; + ta.focus(); + }); + } else { + // No selection: insert empty marker pair, cursor inside + onChange(v.substring(0, start) + marker + marker + v.substring(start)); + requestAnimationFrame(() => { + ta.selectionStart = ta.selectionEnd = start + mLen; + ta.focus(); + }); + } +} + /** Parse the line at cursor position into its structural components */ function blLineAt(val: string, pos: number) { const s = val.lastIndexOf('\n', pos - 1) + 1; @@ -116,6 +159,22 @@ function BulletEditor({ } }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps + // ── Inline formatting: Bold / Italic / Underline ─────────────────────────── + const wrapBold = useCallback(() => { + const ta = textareaRef.current; + if (ta) blWrapFormat(ta, ta.value, '**', onChange); + }, [onChange]); + + const wrapItalic = useCallback(() => { + const ta = textareaRef.current; + if (ta) blWrapFormat(ta, ta.value, '*', onChange); + }, [onChange]); + + const wrapUnderline = useCallback(() => { + const ta = textareaRef.current; + if (ta) blWrapFormat(ta, ta.value, '__', onChange); + }, [onChange]); + // ── Toggle numbered list ──────────────────────────────────────────────────── const toggleNumbered = useCallback(() => { const ta = textareaRef.current; @@ -146,6 +205,15 @@ function BulletEditor({ const v = ta.value; const info = blLineAt(v, pos); + // Ctrl+B / Ctrl+I / Ctrl+U → inline formatting + if ((e.ctrlKey || e.metaKey) && ['b', 'i', 'u'].includes(e.key)) { + e.preventDefault(); + if (e.key === 'b') blWrapFormat(ta, v, '**', onChange); + else if (e.key === 'i') blWrapFormat(ta, v, '*', onChange); + else if (e.key === 'u') blWrapFormat(ta, v, '__', onChange); + return; + } + // Tab → indent (+2 spaces at line start) // Shift+Tab → outdent (-2 spaces from line start) if (e.key === 'Tab') { @@ -218,6 +286,31 @@ function BulletEditor({ 1. Liste + + + + Tab = einrücken · ⇧Tab = ausrücken