From 6a855fc32c0ca17b67d232bf5df35d23f09f28da Mon Sep 17 00:00:00 2001 From: Sandro Date: Sat, 31 Oct 2026 00:19:19 +0100 Subject: [PATCH] feat(workspace): slash commands ranked by name, taller link cards with a link marker - /li lists /link first: name prefix, aliases, name, then description - link cards about twice as tall; the bare address below becomes an accent link marker; the link chip bar under bullets is gone, bare web addresses in text are links themselves chore(release): v0.14.3 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- .../features/workspace/LinkPreviewCard.tsx | 8 +- .../src/features/workspace/OutlinerBlock.tsx | 93 +++++++------------ .../__tests__/OutlinerBlock.caret.test.tsx | 10 ++ .../src/lib/__tests__/linkPreview.test.ts | 8 ++ frontend/src/lib/__tests__/markdown.test.tsx | 13 +++ frontend/src/lib/linkPreview.ts | 11 +++ frontend/src/lib/markdown.tsx | 22 ++++- 10 files changed, 109 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78939ea..6598461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.14.3] - 2026-10-31 + +### Changed +- Slash-Menü: Befehle, deren Name mit dem Getippten beginnt, stehen oben (`/li` → zuerst `/link`), danach Aliase, dann Treffer im Namen, zuletzt in der Beschreibung. +- Link-Vorschau: Karte und Bild etwa doppelt so hoch; statt der nackten Adresse steht darunter eine kleine Link-Markierung (Domain und Pfad) in der Akzentfarbe des Themes. +- Links in normalen Punkten: Die grüne Markierungsleiste unter dem Punkt entfällt; Web-Adressen im Text sind jetzt direkt anklickbar (Satzzeichen am Ende gehören nicht zum Link). + ## [0.14.2] - 2026-10-31 ### Fixed @@ -345,7 +352,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...dev +[0.14.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...v0.14.3 [0.14.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.1...v0.14.2 [0.14.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.0...v0.14.1 [0.14.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.13.0...v0.14.0 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 333a94d..7e82406 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.14.2", + "version": "0.14.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.14.2", + "version": "0.14.3", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 3658ed2..8940d7a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.14.2", + "version": "0.14.3", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/workspace/LinkPreviewCard.tsx b/frontend/src/features/workspace/LinkPreviewCard.tsx index 654d693..fc8a2fd 100644 --- a/frontend/src/features/workspace/LinkPreviewCard.tsx +++ b/frontend/src/features/workspace/LinkPreviewCard.tsx @@ -52,9 +52,9 @@ export const LinkPreviewCard: React.FC = ({ url, canEdit, href={href} target="_blank" rel="noopener noreferrer" - className="flex items-stretch gap-3 rounded-card border border-border bg-panel hover:bg-panel-elevated transition-colors overflow-hidden no-underline" + className="flex items-stretch gap-3 min-h-[6.5rem] rounded-card border border-border bg-panel hover:bg-panel-elevated transition-colors overflow-hidden no-underline" > -
+
{icon ? ( @@ -64,9 +64,9 @@ export const LinkPreviewCard: React.FC = ({ url, canEdit, {preview.siteName}
{preview.title &&
{preview.title}
} - {preview.description &&
{preview.description}
} + {preview.description &&
{preview.description}
}
- {image && } + {image && } {canEdit && (
)} - {/* Rich Tokens Bar (Wiki-Links & #Tags) if present */} - {hasSpecialTokens && !isFocused && ( -
- {tokens.map((part, idx) => { - if (part.startsWith('[[')) return null; - const mdMatch = part.match(/^\[(.*?)\]\((https?:\/\/.*?)\)$/); - if (mdMatch && safeHref(mdMatch[2])) { - const label = mdMatch[1]; - const url = safeHref(mdMatch[2]) as string; - return ( - - - {label || url} - - ); - } - // The preview card above already shows this address - if (/^https?:\/\//.test(part) && safeHref(part) && part !== previewUrl) { - return ( - - - {part} - - ); - } - return null; - })} -
- )} - {/* Floating Slash Commands Menu */} {slashSearch !== null && canEdit && (
{ expect(onCreateBelow).toHaveBeenCalledWith('b1', ''); }); + it('lists commands whose name starts with the typed text first (/li → /link before /number)', () => { + const block = { id: 'b1', type: 'bullet', content: '', level: 0 } as DocumentBlock; + const { container } = renderBlock(block); + const ta = container.querySelector('textarea') as HTMLTextAreaElement; + fireEvent.change(ta, { target: { value: '/li', selectionStart: 3, selectionEnd: 3 } }); + const labels = [...container.querySelectorAll('button')].map((b) => b.textContent || '').filter((t) => t.startsWith('/')); + expect(labels[0]).toMatch(/^\/link/); + expect(labels.findIndex((l) => l.startsWith('/number'))).toBeGreaterThan(0); + }); + it('keeps an empty indented bullet on its level when another bullet on that level follows', () => { const onIndent = vi.fn(); const onCreateBelow = vi.fn(); diff --git a/frontend/src/lib/__tests__/linkPreview.test.ts b/frontend/src/lib/__tests__/linkPreview.test.ts index c266781..b91dc20 100644 --- a/frontend/src/lib/__tests__/linkPreview.test.ts +++ b/frontend/src/lib/__tests__/linkPreview.test.ts @@ -21,3 +21,11 @@ describe('link preview helpers', () => { expect(linkCardUrl(hidePreviewContent('https://koch.de/x', 'https://koch.de/x', 'T'))).toBeNull(); }); }); + +describe('linkHost', () => { + it('shows host and path without scheme and www', async () => { + const { linkHost } = await import('../linkPreview'); + expect(linkHost('https://www.example.com/')).toBe('example.com'); + expect(linkHost('https://example.com/blog/post')).toBe('example.com/blog/post'); + }); +}); diff --git a/frontend/src/lib/__tests__/markdown.test.tsx b/frontend/src/lib/__tests__/markdown.test.tsx index eb8fcff..014a260 100644 --- a/frontend/src/lib/__tests__/markdown.test.tsx +++ b/frontend/src/lib/__tests__/markdown.test.tsx @@ -17,6 +17,19 @@ describe('renderInlineMarkdown', () => { expect(container.textContent).toBe('Der Block services startet'); }); + it('links bare web addresses without the trailing punctuation and keeps markdown links intact', () => { + const { container } = render( +
{renderInlineMarkdown('Siehe https://example.com/a_b_c. und [Doku](https://example.org/x) oder https://x.de/#frag')}
+ ); + const links = [...container.querySelectorAll('a')].map((a) => [a.textContent, a.getAttribute('href')]); + expect(links).toEqual([ + ['https://example.com/a_b_c', 'https://example.com/a_b_c'], + ['Doku', 'https://example.org/x'], + ['https://x.de/#frag', 'https://x.de/#frag'], + ]); + expect(container.textContent).toBe('Siehe https://example.com/a_b_c. und Doku oder https://x.de/#frag'); + }); + it('renders italic text with tag', () => { const { container } = render(
{renderInlineMarkdown('Text mit *kursivem* Wort')}
); const em = container.querySelector('em'); diff --git a/frontend/src/lib/linkPreview.ts b/frontend/src/lib/linkPreview.ts index acf3672..1c4d923 100644 --- a/frontend/src/lib/linkPreview.ts +++ b/frontend/src/lib/linkPreview.ts @@ -25,6 +25,17 @@ export function linkCardUrl(content: string): string | null { } /** Replaces the address by a named link, which shows no preview. */ +/** "example.com/pfad" for a short link marker (no scheme, no www.) */ +export function linkHost(url: string): string { + try { + const u = new URL(url); + const path = u.pathname === '/' ? '' : u.pathname; + return u.host.replace(/^www\./, '') + path; + } catch { + return url; + } +} + export function hidePreviewContent(content: string, url: string, title: string): string { const label = (title || new URL(url).hostname).replace(/[[\]]/g, '').trim() || url; return content.replace(url, `[${label}](${url})`); diff --git a/frontend/src/lib/markdown.tsx b/frontend/src/lib/markdown.tsx index a78dce0..773ecab 100644 --- a/frontend/src/lib/markdown.tsx +++ b/frontend/src/lib/markdown.tsx @@ -22,7 +22,7 @@ function renderInlineTokens(text: string, options?: InlineMarkdownOptions): Reac const tagAlt = options?.onTagClick ? '|#[a-zA-Z0-9äöüÄÖÜß_-]+' : ''; const regex = new RegExp( - '(\\[\\[[^\\]]+\\]\\]|`[^`]+`|\\*\\*[^*]+\\*\\*|__[^_]+__|~~[^~]+~~|\\*[^*]+\\*|_[^_]+_|\\[([^\\]]+)\\]\\(([^)]+)\\)' + tagAlt + ')', + '(\\[\\[[^\\]]+\\]\\]|https?:\\/\\/[^\\s<>()\\[\\]]+[^\\s<>()\\[\\].,;:!?"\']|`[^`]+`|\\*\\*[^*]+\\*\\*|__[^_]+__|~~[^~]+~~|\\*[^*]+\\*|_[^_]+_|\\[([^\\]]+)\\]\\(([^)]+)\\)' + tagAlt + ')', 'g' ); @@ -37,7 +37,25 @@ function renderInlineTokens(text: string, options?: InlineMarkdownOptions): Reac const token = match[0]; const key = `${match.index}-${token}`; - if (token.startsWith('[[') && token.endsWith(']]')) { + if (/^https?:\/\//.test(token)) { + const href = safeHref(token); + elements.push( + href ? ( + e.stopPropagation()} + > + {token} + + ) : ( + token + ) + ); + } else if (token.startsWith('[[') && token.endsWith(']]')) { const link = parseWikiLink(token.slice(2, -2)); const onClick = options?.onWikiLinkClick; elements.push(