feat(workspace): numbered lists and reliable markdown paste
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m5s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m5s
- block type 'numbered' (/number): 1., a., i. by nesting, continues on Enter, an empty item ends the list; markdown export/import as 1. - pasting: parser moved to lib/pasteOutline with tests; blank lines split paragraphs, numbered lists keep their type, indentation levels follow the widths in use (2, 3 or 4 spaces, tabs) - text that stays in the focused block after pasting (a paragraph with line breaks) was dropped; the block now takes it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9008d7c915
commit
2bfe74e66b
13 files changed
+286
-176
No files matched your search
@@ -12,6 +12,7 @@ import {
|
||||
Heading2,
|
||||
Heading3,
|
||||
ListTodo,
|
||||
ListOrdered,
|
||||
Minus,
|
||||
MessageSquare,
|
||||
ExternalLink,
|
||||
@@ -54,6 +55,7 @@ export interface SlashCommand {
|
||||
|
||||
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'] },
|
||||
{ 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: 'h2', label: '/h2', desc: 'Abschnitts-Überschrift', icon: <Heading2 className="w-3.5 h-3.5" /> },
|
||||
@@ -98,6 +100,8 @@ export interface OutlinerBlockProps {
|
||||
isTargeted?: boolean;
|
||||
registerTextareaRef: (blockId: string, el: HTMLTextAreaElement | null) => void;
|
||||
onPasteMultiLines?: (blockId: string, textBefore: string, textAfter: string, rawLines: string[]) => void;
|
||||
/** Number or letter of a numbered-list item ("1.", "b.", "iii.") */
|
||||
listMarker?: string;
|
||||
lockedBy?: string; // someone else is editing this block
|
||||
canAppendBelow?: boolean; // the user may edit the note, so Enter in a locked block still adds one below
|
||||
onResolveConflict?: (blockId: string, accept: boolean) => void;
|
||||
@@ -145,6 +149,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
isTargeted,
|
||||
registerTextareaRef,
|
||||
onPasteMultiLines,
|
||||
listMarker,
|
||||
lockedBy,
|
||||
canAppendBelow,
|
||||
onResolveConflict,
|
||||
@@ -600,6 +605,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
}
|
||||
|
||||
isPastingRef.current = true;
|
||||
// Pasted text that stays in this block arrives as a prop change while the field has focus: take it
|
||||
acceptRemoteUntilRef.current = Date.now() + 1000;
|
||||
onPasteMultiLinesRef.current?.(block.id, textBefore, textAfter, rawLines);
|
||||
};
|
||||
|
||||
@@ -813,6 +820,11 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
onIndentBlockRef.current(block.id, -1);
|
||||
return;
|
||||
}
|
||||
// Enter in an empty numbered item at the top ends the list
|
||||
if (block.type === 'numbered') {
|
||||
flushDebounce('', { type: 'text' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const start = e.currentTarget.selectionStart ?? localContent.length;
|
||||
@@ -970,7 +982,9 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
draggable={canEdit}
|
||||
onDragStart={() => canEdit && onDragStartBlock?.(index)}
|
||||
onClick={() => onZoomBlock?.(block.id)}
|
||||
className={`w-4 h-4 flex items-center justify-center rounded-btn group/bullet transition-transform ${
|
||||
className={`${
|
||||
block.type === 'numbered' && listMarker ? 'w-4 h-4 justify-end overflow-visible' : 'w-4 h-4 justify-center'
|
||||
} flex items-center rounded-btn group/bullet transition-transform ${
|
||||
canEdit ? 'cursor-grab active:cursor-grabbing' : 'cursor-pointer'
|
||||
}`}
|
||||
title={
|
||||
@@ -979,6 +993,15 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
: 'Klick: In diesen Ast hineinzoomen (Fokus-Modus)'
|
||||
}
|
||||
>
|
||||
{block.type === 'numbered' && listMarker ? (
|
||||
<span
|
||||
className={`text-xs tabular-nums font-medium leading-none whitespace-nowrap transition-colors ${
|
||||
activeFocus ? 'text-main' : 'text-muted group-hover/bullet:text-accent'
|
||||
}`}
|
||||
>
|
||||
{listMarker}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all duration-150 ${
|
||||
block.collapsed && hasChildren
|
||||
@@ -992,6 +1015,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
: 'opacity-100'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* If To-Do block: checkbox alongside bullet */}
|
||||
|
||||
Reference in new issue
Block a user