feat(workspace): turn a branch into its own note, merge notes
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m56s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m56s
- Extract: button on the bullet (desktop) or /extract (/notiz). The bullet becomes the title (hashtags stay at the link), its children the content of a new note in the same folder with the source's shares (a foreign source owner gets write access); the branch becomes [[title]] through the operation log, so open editors follow. - Merge: drop a note onto another in the sidebar or use "Mit anderer Notiz zusammenführen…" in the note header. The own source is appended as an h2 section with its bullets below and goes to the trash; the toast offers a complete undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c26eb2960
commit
4d1b37c956
15 files changed
+934
-9
No files matched your search
@@ -23,6 +23,7 @@ import {
|
||||
Image as ImageIcon,
|
||||
Mic,
|
||||
AudioLines,
|
||||
FileOutput,
|
||||
} from 'lucide-react';
|
||||
import { DocumentBlock, BlockType } from '../../lib/types';
|
||||
import { LinkModal } from './LinkModal';
|
||||
@@ -41,7 +42,7 @@ import { api } from '../../lib/api';
|
||||
import { safeHref } from '../../lib/safeUrl';
|
||||
|
||||
export interface SlashCommand {
|
||||
type: BlockType | 'link' | 'dictation';
|
||||
type: BlockType | 'link' | 'dictation' | 'extract';
|
||||
label: string;
|
||||
desc: string;
|
||||
icon: React.ReactNode;
|
||||
@@ -60,6 +61,7 @@ const SLASH_COMMANDS: SlashCommand[] = [
|
||||
{ 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: 'divider', label: '/divider', desc: 'Trennlinie', icon: <Minus className="w-3.5 h-3.5" /> },
|
||||
{ type: 'extract', label: '/extract', desc: 'Ast als eigene Notiz', icon: <FileOutput className="w-3.5 h-3.5" />, aliases: ['notiz', 'auslagern'] },
|
||||
{ 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 oder Audiodatei', icon: <Mic className="w-3.5 h-3.5" />, media: true, aliases: ['sprachnotiz'] },
|
||||
{ type: 'dictation', label: '/diktat', desc: 'Sprechen statt tippen', icon: <AudioLines className="w-3.5 h-3.5" />, media: true, aliases: ['dictate'] },
|
||||
@@ -76,6 +78,8 @@ export interface OutlinerBlockProps {
|
||||
canEdit: boolean;
|
||||
levelOffset?: number;
|
||||
onZoomBlock?: (blockId: string) => void;
|
||||
/** Turns this bullet and its children into a note of its own */
|
||||
onExtractBlock?: (blockId: string) => void;
|
||||
onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void;
|
||||
onCreateBlockBelow: (afterBlockId: string, newContent?: string) => void;
|
||||
onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void;
|
||||
@@ -123,6 +127,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
canEdit,
|
||||
levelOffset = 0,
|
||||
onZoomBlock,
|
||||
onExtractBlock,
|
||||
onUpdateBlock,
|
||||
onCreateBlockBelow,
|
||||
onDeleteBlock,
|
||||
@@ -362,11 +367,13 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
(c) =>
|
||||
(!c.media || emptyBullet) &&
|
||||
(c.type !== 'dictation' || ['server', 'browser'].includes(currentVoiceConfig().mode)) &&
|
||||
// The bullet's text becomes the title of the new note
|
||||
(c.type !== 'extract' || (!emptyBullet && !!onExtractBlock)) &&
|
||||
(c.label.toLowerCase().includes(cmdQuery) ||
|
||||
c.desc.toLowerCase().includes(cmdQuery) ||
|
||||
(c.aliases || []).some((a) => a.includes(cmdQuery)))
|
||||
);
|
||||
}, [slashSearch, localContent]);
|
||||
}, [slashSearch, localContent, onExtractBlock]);
|
||||
|
||||
const applySlashCommand = (cmd: SlashCommand) => {
|
||||
setSlashSearch(null);
|
||||
@@ -377,6 +384,13 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
setShowLinkModal(true);
|
||||
return;
|
||||
}
|
||||
if (cmd.type === 'extract') {
|
||||
setLocalContent(clean);
|
||||
localContentRef.current = clean;
|
||||
flushDebounce(clean);
|
||||
onExtractBlock?.(block.id);
|
||||
return;
|
||||
}
|
||||
if (cmd.media) {
|
||||
// The typed "/image" must not stay in the synced text; the block type changes only after the upload
|
||||
setLocalContent('');
|
||||
@@ -1272,6 +1286,18 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Branch into its own note (desktop; phones use /extract) */}
|
||||
{canEdit && onExtractBlock && localContent.trim() !== '' && !mediaPicker && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onExtractBlock(block.id)}
|
||||
className="hidden sm:block opacity-0 group-hover:opacity-100 focus:opacity-100 p-0.5 text-faint hover:text-accent rounded-btn transition-opacity shrink-0"
|
||||
title="In eigene Notiz umwandeln (mit allen Unterpunkten)"
|
||||
>
|
||||
<FileOutput className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Zoom / Focus button on hover */}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in new issue
Block a user