feat: F4.8b the documentation mode in the editor

Java's third mode button, the sidebar's text and structure field (dropped
with its middle under the pointer), items over the network that are moved,
resized by Java's rule, typed into on their Y.Text, coloured and set in
size and style from Java's menu, deleted; Java's paint per mode, measured
in F4.8a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 11:27:25 +00:00
co-authored by Claude Opus 5.5
parent 56cdb4926e
commit de03f6bb96
17 changed files with 1034 additions and 42 deletions
@@ -0,0 +1,79 @@
// The field a focused text is typed into (Java's JDocuTextArea): over the
// text, at its size, in its font and colour, scaled with the view. It
// follows the document's Y.Text — what others type comes in, the caret kept —
// and writes what is typed with this editor's origin. Escape ends the focus;
// Ctrl+Delete removes the text, as in Java (Delete alone types).
import { useEffect, useRef } from 'react';
import type * as Y from 'yjs';
import { replaceText, type DocumentationView } from '@mdg-filius/doc';
import type { ScreenFrame } from '../canvas/Canvas.js';
import { fontFamily, textColor } from '../canvas/documentationPaint.js';
import { followText } from '../desktop/textBinding.js';
import { useTranslate } from '../i18n/i18n.js';
export interface DocumentationTextProps {
readonly item: DocumentationView;
readonly frame: ScreenFrame;
readonly text: Y.Text;
readonly origin: object;
readonly onDone: () => void;
readonly onRemove: () => void;
}
export function DocumentationText({ item, frame, text, origin, onDone, onRemove }: DocumentationTextProps) {
const t = useTranslate();
const area = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const element = area.current;
if (element === null) return undefined;
const stop = followText(element, text, origin);
// Java gives a text the focus when it gets it (a new one at once).
element.focus();
return stop;
}, [text, origin]);
return (
<textarea
ref={area}
aria-label={t('docu.text')}
data-doc-text={item.id}
spellCheck={false}
onInput={(event) => {
const next = event.currentTarget.value;
text.doc?.transact(() => replaceText(text, next), origin);
}}
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
onDone();
} else if (event.key === 'Delete' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
onRemove();
}
}}
style={{
position: 'absolute',
left: frame.left,
top: frame.top,
width: item.width,
height: item.height,
transform: `scale(${frame.scale})`,
transformOrigin: 'top left',
boxSizing: 'border-box',
margin: 0,
padding: 1,
border: 'none',
outline: 'none',
resize: 'none',
overflow: 'hidden',
background: 'transparent',
color: textColor(item.color),
font: `${item.italic ? 'italic ' : ''}${item.bold ? 'bold ' : ''}${item.size}px ${fontFamily(item.font)}`,
lineHeight: 'normal',
whiteSpace: 'pre-wrap',
overflowWrap: 'break-word',
zIndex: 3,
}}
/>
);
}