feat(workspace): bar above the phone keyboard to indent, outdent and move bullets
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m33s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 11:09:39 +01:00
1 parent 32b79b5d62
commit c6a2cfb85c
9 files changed
+183 -7

No files matched your search

@@ -3,6 +3,8 @@ import { DocumentBlock, BlockType } from '../../lib/types';
import { OutlinerBlock } from './OutlinerBlock';
import { parsePastedLines } from '../../lib/pasteOutline';
import { numberedLabels } from '../../lib/outlineList';
import { flushSync } from 'react-dom';
import { MobileOutlineBar, isTouchOnly } from './MobileOutlineBar';
export interface OutlinerTreeHandle {
focusFirstBlock: () => void;
@@ -24,6 +26,8 @@ export interface OutlinerTreeProps {
onInsertBlocks?: (afterBlockId: string, updatedCurrent: DocumentBlock, newBlocks: DocumentBlock[]) => void;
onDeleteBlock: (blockId: string) => void;
onMoveBlock: (blockId: string, newIndex: number, level?: number) => void;
/** Swap a bullet (with children) with its previous / next sibling (phone bar ↑ ↓) */
onMoveSibling?: (blockId: string, direction: -1 | 1) => void;
onOpenTask?: (taskId: string) => void;
onTagClick?: (tag: string) => void;
onWikiLinkClick?: (pageTitle: string, blockId?: string) => void;
@@ -51,6 +55,7 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
onInsertBlocks,
onDeleteBlock,
onMoveBlock,
onMoveSibling,
onOpenTask,
onTagClick,
onWikiLinkClick,
@@ -612,6 +617,39 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
[blocks, draggedIndex, onMoveBlock, lockHolder, onLockedAction]
);
// Phone bar: runs the action at once and puts the focus back into the same bullet within the same tap
// (React may move the field in the DOM, which drops the focus; iOS reopens the keyboard only inside a tap)
const runOnFocused = (action: (blockId: string) => void) => {
const id = focusedBlockId;
if (!id) return;
const holder = lockHolder(id);
if (holder) {
onLockedAction?.(holder);
return;
}
const before = textareaMapRef.current.get(id);
const caret = before ? before.selectionStart : null;
flushSync(() => action(id));
const el = textareaMapRef.current.get(id);
if (!el) return;
if (document.activeElement !== el) {
el.focus({ preventScroll: true });
if (caret !== null) {
try {
el.setSelectionRange(caret, caret);
} catch {
// ignore
}
}
}
// Keep the bullet visible above the bar (about 56 px) and the keyboard
const vv = window.visualViewport;
const visibleBottom = (vv ? vv.height + vv.offsetTop : window.innerHeight) - 64;
const rect = el.getBoundingClientRect();
if (rect.bottom > visibleBottom || rect.top < 0) el.scrollIntoView({ block: 'center' });
};
const showMobileBar = isTouchOnly && canEdit && !!focusedBlockId && blocks.some((b) => b.id === focusedBlockId);
useEffect(() => {
const clearDrag = () => {
setDraggedIndex(null);
@@ -707,6 +745,14 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
onDrop={() => handleDropBlock(blocks.length)}
/>
)}
{showMobileBar && (
<MobileOutlineBar
onOutdent={() => runOnFocused((id) => handleIndentBlock(id, -1))}
onIndent={() => runOnFocused((id) => handleIndentBlock(id, 1))}
onMoveUp={() => runOnFocused((id) => onMoveSibling?.(id, -1))}
onMoveDown={() => runOnFocused((id) => onMoveSibling?.(id, 1))}
/>
)}
</div>
);
});