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
@@ -50,27 +50,34 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
|
||||
const textareaMapRef = useRef<Map<string, HTMLTextAreaElement>>(new Map());
|
||||
const pendingFocusRef = useRef<{ blockId: string; cursor: 'start' | 'end' | 'select' } | null>(null);
|
||||
const pendingFocusRef = useRef<{ blockId: string; cursor: 'start' | 'end' | 'select' | number } | null>(null);
|
||||
|
||||
const focusBlockInput = useCallback(
|
||||
(blockId: string, cursor: 'start' | 'end' | 'select' = 'end') => {
|
||||
(blockId: string, cursor: 'start' | 'end' | 'select' | number = 'end') => {
|
||||
setFocusedBlockId(blockId);
|
||||
const el = textareaMapRef.current.get(blockId);
|
||||
if (el) {
|
||||
requestAnimationFrame(() => {
|
||||
const apply = () => {
|
||||
el.focus();
|
||||
try {
|
||||
if (cursor === 'select') {
|
||||
el.select();
|
||||
} else {
|
||||
const pos = cursor === 'end' ? el.value.length : 0;
|
||||
const pos =
|
||||
typeof cursor === 'number'
|
||||
? Math.min(cursor, el.value.length)
|
||||
: cursor === 'end'
|
||||
? el.value.length
|
||||
: 0;
|
||||
el.setSelectionRange(pos, pos);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
};
|
||||
// A numeric position refers to content that was just changed, so wait one extra frame for it to render
|
||||
requestAnimationFrame(() => (typeof cursor === 'number' ? requestAnimationFrame(apply) : apply()));
|
||||
} else {
|
||||
pendingFocusRef.current = { blockId, cursor };
|
||||
}
|
||||
@@ -514,6 +521,30 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
||||
[blocks, canEdit, focusBlockInput, levelOffset, onDeleteBlock, onUpdateBlock]
|
||||
);
|
||||
|
||||
// Backspace at the start of a non-empty block: append its text to the previous visible block
|
||||
const handleMergeWithPrevious = useCallback(
|
||||
(blockId: string, content: string) => {
|
||||
if (!canEdit) return false;
|
||||
const mergeable = ['text', 'paragraph', 'bullet', 'todo', 'h1', 'h2', 'h3', 'callout'];
|
||||
const visIdx = visibleEntries.findIndex((e) => e.block.id === blockId);
|
||||
if (visIdx <= 0) return false;
|
||||
const prev = visibleEntries[visIdx - 1].block;
|
||||
const idx = blocks.findIndex((b) => b.id === blockId);
|
||||
const cur = blocks[idx];
|
||||
if (!cur || !mergeable.includes(prev.type) || !mergeable.includes(cur.type)) return false;
|
||||
// Blocks with children would leave them orphaned at a wrong indent level
|
||||
const next = blocks[idx + 1];
|
||||
if (next && (next.level || 0) > (cur.level || 0)) return false;
|
||||
|
||||
const joinPos = (prev.content || '').length;
|
||||
onUpdateBlock({ ...prev, content: (prev.content || '') + content }, true);
|
||||
onDeleteBlock(cur.id);
|
||||
focusBlockInput(prev.id, joinPos);
|
||||
return true;
|
||||
},
|
||||
[blocks, canEdit, focusBlockInput, onDeleteBlock, onUpdateBlock, visibleEntries]
|
||||
);
|
||||
|
||||
const handleIndentBlock = useCallback(
|
||||
(blockId: string, delta: 1 | -1) => {
|
||||
if (!canEdit) return;
|
||||
@@ -672,6 +703,7 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
||||
onCreateBlockBelow={handleCreateBlockBelow}
|
||||
onPasteMultiLines={handlePasteMultiLines}
|
||||
onDeleteBlock={handleDeleteBlock}
|
||||
onMergeWithPrevious={handleMergeWithPrevious}
|
||||
onIndentBlock={handleIndentBlock}
|
||||
onNavigateFocus={handleNavigateFocus}
|
||||
onFocusBlock={setFocusedBlockId}
|
||||
|
||||
Reference in new issue
Block a user