fix(workspace): English slash commands, short texts, wider menu

/image replaces /bild (aliases bild/foto keep working), shorter
descriptions for /link, /image, /audio, /diktat; the menu is wider, the
command never wraps and long descriptions are cut off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 23:47:38 +01:00
1 parent 1f63e08a73
commit 1632dd22c4
3 files changed
+17 -13

No files matched your search

+1 -1
View File
@@ -200,7 +200,7 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
</div> </div>
<div className="p-1.5 bg-panel rounded border border-border flex items-center gap-1.5 text-main col-span-2"> <div className="p-1.5 bg-panel rounded border border-border flex items-center gap-1.5 text-main col-span-2">
<span className="text-accent font-bold">🖼️</span> <span className="text-accent font-bold">🖼️</span>
<span>/bild (Bild hochladen, fotografieren, einfügen oder per Link; nur im leeren Punkt)</span> <span>/image oder /bild (Bild hochladen, fotografieren, einfügen oder per Link; nur im leeren Punkt)</span>
</div> </div>
<div className="p-1.5 bg-panel rounded border border-border flex items-center gap-1.5 text-main col-span-2"> <div className="p-1.5 bg-panel rounded border border-border flex items-center gap-1.5 text-main col-span-2">
<span className="text-accent font-bold">🎙️</span> <span className="text-accent font-bold">🎙️</span>
@@ -8,7 +8,7 @@ interface ImagePickerProps {
onCancel: () => void; onCancel: () => void;
} }
// Inline box an empty bullet turns into after /bild: pick a file (or the camera on phones), drop or // Inline box an empty bullet turns into after /image: pick a file (or the camera on phones), drop or
// paste an image, or give a link the server copies. Nothing is synced until the upload succeeded. // paste an image, or give a link the server copies. Nothing is synced until the upload succeeded.
export const ImagePicker: React.FC<ImagePickerProps> = ({ docId, onDone, onCancel }) => { export const ImagePicker: React.FC<ImagePickerProps> = ({ docId, onDone, onCancel }) => {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
@@ -45,20 +45,22 @@ export interface SlashCommand {
icon: React.ReactNode; icon: React.ReactNode;
/** Only offered in an empty bullet; the bullet turns into an inline picker first */ /** Only offered in an empty bullet; the bullet turns into an inline picker first */
media?: boolean; media?: boolean;
/** Further names that find the command (e.g. German ones) */
aliases?: string[];
} }
const SLASH_COMMANDS: SlashCommand[] = [ const SLASH_COMMANDS: SlashCommand[] = [
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: <ListTodo className="w-3.5 h-3.5" /> }, { type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: <ListTodo className="w-3.5 h-3.5" /> },
{ type: 'link', label: '/link', desc: 'Notiz verknüpfen oder Web-Link einfügen', icon: <LinkIcon className="w-3.5 h-3.5" /> }, { type: 'link', label: '/link', desc: 'Notiz oder Weblink', icon: <LinkIcon className="w-3.5 h-3.5" /> },
{ type: 'h1', label: '/h1', desc: 'Große Überschrift', icon: <Heading1 className="w-3.5 h-3.5" /> }, { type: 'h1', label: '/h1', desc: 'Große Überschrift', icon: <Heading1 className="w-3.5 h-3.5" /> },
{ type: 'h2', label: '/h2', desc: 'Abschnitts-Überschrift', icon: <Heading2 className="w-3.5 h-3.5" /> }, { type: 'h2', label: '/h2', desc: 'Abschnitts-Überschrift', icon: <Heading2 className="w-3.5 h-3.5" /> },
{ type: 'h3', label: '/h3', desc: 'Unterabschnitt', icon: <Heading3 className="w-3.5 h-3.5" /> }, { type: 'h3', label: '/h3', desc: 'Unterabschnitt', icon: <Heading3 className="w-3.5 h-3.5" /> },
{ type: 'code', label: '/code', desc: 'Code-Block mit Syntax', icon: <Code className="w-3.5 h-3.5" /> }, { type: 'code', label: '/code', desc: 'Code-Block mit Syntax', icon: <Code className="w-3.5 h-3.5" /> },
{ type: 'callout', label: '/callout', desc: 'Hinweisbox', icon: <MessageSquare className="w-3.5 h-3.5" /> }, { type: 'callout', label: '/callout', desc: 'Hinweisbox', icon: <MessageSquare className="w-3.5 h-3.5" /> },
{ type: 'divider', label: '/divider', desc: 'Trennlinie', icon: <Minus className="w-3.5 h-3.5" /> }, { type: 'divider', label: '/divider', desc: 'Trennlinie', icon: <Minus className="w-3.5 h-3.5" /> },
{ type: 'image', label: '/bild', desc: 'Bild hochladen, fotografieren oder verlinken (image)', icon: <ImageIcon className="w-3.5 h-3.5" />, media: true }, { type: 'image', label: '/image', desc: 'Bilder hochladen oder verlinken', icon: <ImageIcon className="w-3.5 h-3.5" />, media: true, aliases: ['bild', 'foto'] },
{ type: 'audio', label: '/audio', desc: 'Sprachnotiz aufnehmen oder Audiodatei hochladen', icon: <Mic className="w-3.5 h-3.5" />, media: true }, { type: 'audio', label: '/audio', desc: 'Sprachnotiz oder Audiodatei', icon: <Mic className="w-3.5 h-3.5" />, media: true, aliases: ['sprachnotiz'] },
{ type: 'dictation', label: '/diktat', desc: 'Sprechen statt tippen: Text direkt in den Punkt', icon: <AudioLines className="w-3.5 h-3.5" />, media: true }, { type: 'dictation', label: '/diktat', desc: 'Sprechen statt tippen', icon: <AudioLines className="w-3.5 h-3.5" />, media: true, aliases: ['dictate'] },
]; ];
export interface OutlinerBlockProps { export interface OutlinerBlockProps {
@@ -149,7 +151,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
// A block someone else is editing can still get the cursor (read-only), e.g. to press Enter at its end // A block someone else is editing can still get the cursor (read-only), e.g. to press Enter at its end
const canFocus = canEdit || (!!lockedBy && !!canAppendBelow); const canFocus = canEdit || (!!lockedBy && !!canAppendBelow);
const [slashSearch, setSlashSearch] = useState<string | null>(null); const [slashSearch, setSlashSearch] = useState<string | null>(null);
// Inline picker an empty bullet turns into after /bild (local only, nothing synced until done) // Inline picker an empty bullet turns into after /image (local only, nothing synced until done)
const [mediaPicker, setMediaPicker] = useState<'image' | 'audio' | 'dictation' | null>(null); const [mediaPicker, setMediaPicker] = useState<'image' | 'audio' | 'dictation' | null>(null);
const [slashIndex, setSlashIndex] = useState(0); const [slashIndex, setSlashIndex] = useState(0);
const [showLinkModal, setShowLinkModal] = useState<boolean>(false); const [showLinkModal, setShowLinkModal] = useState<boolean>(false);
@@ -352,7 +354,9 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
(c) => (c) =>
(!c.media || emptyBullet) && (!c.media || emptyBullet) &&
(c.type !== 'dictation' || ['server', 'browser'].includes(currentVoiceConfig().mode)) && (c.type !== 'dictation' || ['server', 'browser'].includes(currentVoiceConfig().mode)) &&
(c.label.toLowerCase().includes(cmdQuery) || c.desc.toLowerCase().includes(cmdQuery)) (c.label.toLowerCase().includes(cmdQuery) ||
c.desc.toLowerCase().includes(cmdQuery) ||
(c.aliases || []).some((a) => a.includes(cmdQuery)))
); );
}, [slashSearch, localContent]); }, [slashSearch, localContent]);
@@ -366,7 +370,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
return; return;
} }
if (cmd.media) { if (cmd.media) {
// The typed "/bild" must not stay in the synced text; the block type changes only after the upload // The typed "/image" must not stay in the synced text; the block type changes only after the upload
setLocalContent(''); setLocalContent('');
localContentRef.current = ''; localContentRef.current = '';
flushDebounce(''); flushDebounce('');
@@ -1208,7 +1212,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
{/* Floating Slash Commands Menu */} {/* Floating Slash Commands Menu */}
{slashSearch !== null && canEdit && ( {slashSearch !== null && canEdit && (
<div className="absolute left-0 top-8 z-40 w-56 panel-surface shadow-panel border border-border rounded-card p-1"> <div className="absolute left-0 top-8 z-40 w-72 max-w-[calc(100vw-2rem)] panel-surface shadow-panel border border-border rounded-card p-1">
<div className="px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-faint border-b border-border mb-1"> <div className="px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-faint border-b border-border mb-1">
Befehl auswählen Befehl auswählen
</div> </div>
@@ -1227,11 +1231,11 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
: 'text-main hover:bg-panel-elevated' : 'text-main hover:bg-panel-elevated'
}`} }`}
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5 shrink-0">
{cmd.icon} {cmd.icon}
<span className="font-semibold">{cmd.label}</span> <span className="font-semibold whitespace-nowrap">{cmd.label}</span>
</div> </div>
<span className="text-[10px] opacity-75">{cmd.desc}</span> <span className="ml-3 min-w-0 truncate text-right text-[10px] opacity-75">{cmd.desc}</span>
</button> </button>
))} ))}
{filteredSlashCommands.length === 0 && ( {filteredSlashCommands.length === 0 && (