feat(workspace): links to single bullets
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m8s

- copy a link to a bullet from its action bubble or pick a bullet in the
  link dialog; [[Note#^id|Note › excerpt]] opens the note, jumps to and
  highlights the bullet, or says it is gone
- wiki links render inline with their label and are clickable; the
  duplicate chip below the text is gone

test(e2e): run each suite with its own container, image tag and port, so
parallel runs from another working copy no longer remove our server

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 20:42:14 +01:00
1 parent d408e7d307
commit 1a49e6b304
12 files changed
+284 -45

No files matched your search

+29 -2
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { safeHref } from './safeUrl';
import { parseWikiLink, wikiLinkText } from './wikiLink';
/**
* Parses inline markdown:
@@ -9,9 +10,11 @@ import { safeHref } from './safeUrl';
* - `code`
* - [label](url)
* - #hashtag (only when `options.onTagClick` is given), rendered inline as a clickable chip
* - [[wiki link]] shown by its label (clickable with `options.onWikiLinkClick`)
*/
export interface InlineMarkdownOptions {
onTagClick?: (tag: string) => void;
onWikiLinkClick?: (title: string, blockId?: string) => void;
}
function renderInlineTokens(text: string, options?: InlineMarkdownOptions): React.ReactNode[] {
@@ -19,7 +22,7 @@ function renderInlineTokens(text: string, options?: InlineMarkdownOptions): Reac
const tagAlt = options?.onTagClick ? '|#[a-zA-Z0-9äöüÄÖÜß_-]+' : '';
const regex = new RegExp(
'(`[^`]+`|\\*\\*[^*]+\\*\\*|__[^_]+__|~~[^~]+~~|\\*[^*]+\\*|_[^_]+_|\\[([^\\]]+)\\]\\(([^)]+)\\)' + tagAlt + ')',
'(\\[\\[[^\\]]+\\]\\]|`[^`]+`|\\*\\*[^*]+\\*\\*|__[^_]+__|~~[^~]+~~|\\*[^*]+\\*|_[^_]+_|\\[([^\\]]+)\\]\\(([^)]+)\\)' + tagAlt + ')',
'g'
);
@@ -34,7 +37,31 @@ function renderInlineTokens(text: string, options?: InlineMarkdownOptions): Reac
const token = match[0];
const key = `${match.index}-${token}`;
if (token.startsWith('#')) {
if (token.startsWith('[[') && token.endsWith(']]')) {
const link = parseWikiLink(token.slice(2, -2));
const onClick = options?.onWikiLinkClick;
elements.push(
onClick ? (
<span
key={key}
role="button"
tabIndex={-1}
onClick={(e) => {
e.stopPropagation();
onClick(link.title, link.blockId);
}}
className="text-status-info underline decoration-status-info/40 underline-offset-2 hover:decoration-status-info cursor-pointer"
title={link.blockId ? `Zum Punkt in „${link.title}“` : `Notiz öffnen: ${link.title}`}
>
{wikiLinkText(link)}
</span>
) : (
<span key={key} className="text-status-info">
{wikiLinkText(link)}
</span>
)
);
} else if (token.startsWith('#')) {
const prevChar = match.index > 0 ? text[match.index - 1] : '';
if (prevChar && !/[\s(]/.test(prevChar)) {
// e.g. an URL fragment like https://x/#anchor: keep as plain text