feat(workspace): merge bullet on Backspace, render hashtags inline
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m8s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m8s
- Backspace at the start of a non-empty bullet appends its text to the previous visible bullet (skipped for blocks with children and for code/divider/table/progress blocks). - Hashtags are rendered inline as clickable chips instead of a badge row below the text, so lines no longer grow taller. - Targeted/highlighted bullets use a neutral background instead of accent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bc13235da6
commit
c7e05b385c
3 files changed
+104
-29
No files matched your search
@@ -14,7 +14,6 @@ import {
|
||||
ListTodo,
|
||||
Minus,
|
||||
MessageSquare,
|
||||
Hash,
|
||||
ExternalLink,
|
||||
Maximize2,
|
||||
Link as LinkIcon,
|
||||
@@ -57,6 +56,7 @@ export interface OutlinerBlockProps {
|
||||
onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void;
|
||||
onCreateBlockBelow: (afterBlockId: string, newContent?: string) => void;
|
||||
onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void;
|
||||
onMergeWithPrevious?: (blockId: string, content: string) => boolean;
|
||||
onIndentBlock: (blockId: string, delta: 1 | -1) => void;
|
||||
onNavigateFocus: (blockId: string, direction: 'up' | 'down') => void;
|
||||
onFocusBlock?: (blockId: string) => void;
|
||||
@@ -86,6 +86,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
onUpdateBlock,
|
||||
onCreateBlockBelow,
|
||||
onDeleteBlock,
|
||||
onMergeWithPrevious,
|
||||
onIndentBlock,
|
||||
onNavigateFocus,
|
||||
onFocusBlock,
|
||||
@@ -124,6 +125,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
onPasteMultiLinesRef.current = onPasteMultiLines;
|
||||
const onDeleteBlockRef = useRef(onDeleteBlock);
|
||||
onDeleteBlockRef.current = onDeleteBlock;
|
||||
const onMergeWithPreviousRef = useRef(onMergeWithPrevious);
|
||||
onMergeWithPreviousRef.current = onMergeWithPrevious;
|
||||
const onIndentBlockRef = useRef(onIndentBlock);
|
||||
onIndentBlockRef.current = onIndentBlock;
|
||||
const onNavigateFocusRef = useRef(onNavigateFocus);
|
||||
@@ -455,6 +458,28 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 3b. Backspace at the very start of a non-empty block -> append its text to the previous block
|
||||
if (
|
||||
e.key === 'Backspace' &&
|
||||
localContent !== '' &&
|
||||
block.type !== 'code' &&
|
||||
onMergeWithPreviousRef.current &&
|
||||
e.currentTarget.selectionStart === 0 &&
|
||||
e.currentTarget.selectionEnd === 0
|
||||
) {
|
||||
if (onMergeWithPreviousRef.current(block.id, localContent)) {
|
||||
// Text lives in the previous block afterwards, so drop this block's pending save
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setSlashSearch(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Arrow navigation between blocks
|
||||
if (e.key === 'ArrowUp' && !e.shiftKey && !e.altKey) {
|
||||
const el = textareaRef.current;
|
||||
@@ -489,9 +514,9 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
return parts.filter(Boolean);
|
||||
}, [localContent]);
|
||||
|
||||
// #Hashtags are rendered inline inside the text, so only links need the badge bar below
|
||||
const hasSpecialTokens = tokens.some(
|
||||
(t) =>
|
||||
t.startsWith('#') ||
|
||||
(t.startsWith('[[') && t.endsWith(']]')) ||
|
||||
/^\[(.*?)\]\((https?:\/\/.*?)\)$/.test(t) ||
|
||||
/^https?:\/\//.test(t)
|
||||
@@ -503,7 +528,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
isDragTarget
|
||||
? 'border-t-2 border-accent bg-accent/10'
|
||||
: isTargeted
|
||||
? 'bg-accent/15 ring-2 ring-accent'
|
||||
? 'bg-panel-elevated'
|
||||
: activeFocus
|
||||
? 'bg-panel-elevated'
|
||||
: 'hover:bg-panel'
|
||||
@@ -680,7 +705,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
>
|
||||
{/* When actively focused, ghost text is invisible so textarea renders text/caret directly */}
|
||||
<span className={activeFocus ? 'invisible' : ''}>
|
||||
{localContent ? renderInlineMarkdown(localContent) : '\u200b'}
|
||||
{localContent ? renderInlineMarkdown(localContent, { onTagClick }) : '\u200b'}
|
||||
</span>
|
||||
|
||||
{/* Inline Badge and 3-Dots directly behind the text */}
|
||||
@@ -809,20 +834,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
{hasSpecialTokens && !isFocused && (
|
||||
<div className="flex flex-wrap items-center gap-1 mt-0.5">
|
||||
{tokens.map((part, idx) => {
|
||||
if (part.startsWith('#') && part.length > 1) {
|
||||
const tag = part.slice(1);
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={() => onTagClick?.(tag)}
|
||||
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium text-accent bg-accent-dim hover:bg-accent/20 rounded-btn transition-colors"
|
||||
>
|
||||
<Hash className="w-2.5 h-2.5 opacity-80" />
|
||||
<span>{tag}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
if (part.startsWith('[[') && part.endsWith(']]')) {
|
||||
const page = part.slice(2, -2).trim();
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user