feat(workspace): slash commands ranked by name, taller link cards with a link marker
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m22s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m22s
- /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 <noreply@anthropic.com>
This commit is contained in:
1 parent
08ae91fcdb
commit
6a855fc32c
10 files changed
+109
-72
No files matched your search
@@ -37,7 +37,7 @@ import { AudioBlock } from './AudioBlock';
|
||||
import { DictationBlock } from './DictationBlock';
|
||||
import { currentVoiceConfig } from '../../lib/voice';
|
||||
import { LinkPreviewCard } from './LinkPreviewCard';
|
||||
import { linkCardUrl, hidePreviewContent, useLinkPreviewsEnabled } from '../../lib/linkPreview';
|
||||
import { linkCardUrl, hidePreviewContent, linkHost, useLinkPreviewsEnabled } from '../../lib/linkPreview';
|
||||
import { mediaContent, parseMediaContent } from '../../lib/media';
|
||||
import { renderInlineMarkdown } from '../../lib/markdown';
|
||||
import { api } from '../../lib/api';
|
||||
@@ -54,6 +54,17 @@ export interface SlashCommand {
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
// The command name counts most: "/li" finds /link before /number (whose description says "Liste")
|
||||
function slashRank(c: SlashCommand, q: string): number {
|
||||
if (!q) return 0;
|
||||
const name = c.label.slice(1).toLowerCase();
|
||||
if (name.startsWith(q)) return 0;
|
||||
if ((c.aliases || []).some((a) => a.startsWith(q))) return 1;
|
||||
if (name.includes(q)) return 2;
|
||||
if ((c.aliases || []).some((a) => a.includes(q))) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
const SLASH_COMMANDS: SlashCommand[] = [
|
||||
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: <ListTodo className="w-3.5 h-3.5" /> },
|
||||
{ type: 'numbered', label: '/number', desc: 'Nummerierte Liste (1., a., i.)', icon: <ListOrdered className="w-3.5 h-3.5" />, aliases: ['nummer', 'liste', 'ol'] },
|
||||
@@ -465,7 +476,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}).sort((a, b) => slashRank(a, cmdQuery) - slashRank(b, cmdQuery));
|
||||
}, [slashMatchInfo, localContent, onExtractBlock]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1062,21 +1073,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
const visualLevel = Math.max(0, level - (levelOffset || 0));
|
||||
const isEmpty = (!localContent || localContent.trim() === '') && !hasChildren;
|
||||
|
||||
// Parse rich tokens (WikiLinks [[Page]], Markdown Links [Title](url), raw URLs, and #Tags)
|
||||
const tokens = useMemo(() => {
|
||||
const parts = (localContent || '').split(
|
||||
/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/.*?\)|\bhttps?:\/\/[^\s]+|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g
|
||||
);
|
||||
return parts.filter(Boolean);
|
||||
}, [localContent]);
|
||||
|
||||
// #Hashtags are rendered inline inside the text, so only links need the badge bar below
|
||||
// Wiki links are readable and clickable in the text itself, so only web links get a chip
|
||||
const hasSpecialTokens = tokens.some(
|
||||
(t) =>
|
||||
/^\[(.*?)\]\((https?:\/\/.*?)\)$/.test(t) ||
|
||||
(/^https?:\/\//.test(t) && t !== previewUrl)
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1390,7 +1386,24 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
>
|
||||
{/* When actively focused, ghost text is invisible so textarea renders text/caret directly */}
|
||||
<span ref={renderedTextRef} className={activeFocus ? 'invisible' : ''}>
|
||||
{localContent ? renderInlineMarkdown(localContent, { onTagClick, onWikiLinkClick }) : '\u200b'}
|
||||
{previewUrl && !activeFocus ? (
|
||||
// The card above shows the page; the address itself becomes a small link marker
|
||||
<a
|
||||
href={safeHref(previewUrl) || undefined}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium text-accent bg-accent-dim hover:bg-accent/20 no-underline transition-colors"
|
||||
title={previewUrl}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
<span className="truncate max-w-[16rem]">{linkHost(previewUrl)}</span>
|
||||
</a>
|
||||
) : localContent ? (
|
||||
renderInlineMarkdown(localContent, { onTagClick, onWikiLinkClick })
|
||||
) : (
|
||||
'\u200b'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1506,50 +1519,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rich Tokens Bar (Wiki-Links & #Tags) if present */}
|
||||
{hasSpecialTokens && !isFocused && (
|
||||
<div className="flex flex-wrap items-center gap-1 mt-0.5">
|
||||
{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 (
|
||||
<a
|
||||
key={idx}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium text-status-ok bg-status-ok/10 hover:bg-status-ok/20 rounded-btn transition-colors"
|
||||
title={url}
|
||||
>
|
||||
<ExternalLink className="w-2.5 h-2.5 opacity-80" />
|
||||
<span>{label || url}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
// The preview card above already shows this address
|
||||
if (/^https?:\/\//.test(part) && safeHref(part) && part !== previewUrl) {
|
||||
return (
|
||||
<a
|
||||
key={idx}
|
||||
href={safeHref(part) as string}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium text-status-ok bg-status-ok/10 hover:bg-status-ok/20 rounded-btn transition-colors"
|
||||
title={part}
|
||||
>
|
||||
<ExternalLink className="w-2.5 h-2.5 opacity-80" />
|
||||
<span className="truncate max-w-[160px]">{part}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating Slash Commands Menu */}
|
||||
{slashSearch !== null && canEdit && (
|
||||
<div
|
||||
|
||||
Reference in new issue
Block a user