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:
@@ -0,0 +1,130 @@
|
||||
// The documentation mode's sidebar (Java's GUIDocumentationSidebar): „Text“
|
||||
// and „Gliederungsfläche“ with Java's icons. An item is dragged onto the
|
||||
// surface, where it lands with its middle under the pointer, as Java's does;
|
||||
// tapped, it lands in the middle of the view (Java's tap makes nothing).
|
||||
|
||||
import { useRef, useState, type CSSProperties } from 'react';
|
||||
import type { DocumentationItemKind } from '@mdg-filius/core';
|
||||
import { DRAG_THRESHOLD } from '../canvas/Canvas.js';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import addRectangle from '../assets/documentation/add_small.png';
|
||||
import addText from '../assets/documentation/add_text_small.png';
|
||||
|
||||
export interface DocumentationSidebarProps {
|
||||
/** An item dropped at a point of the screen, or tapped (no point). */
|
||||
readonly onItem: (kind: DocumentationItemKind, at: { readonly clientX: number; readonly clientY: number } | null) => void;
|
||||
}
|
||||
|
||||
const ITEMS: readonly { readonly kind: DocumentationItemKind; readonly icon: string; readonly label: string }[] = [
|
||||
{ kind: 'text', icon: addText, label: 'docu.text' },
|
||||
{ kind: 'rectangle', icon: addRectangle, label: 'docu.rectangle' },
|
||||
];
|
||||
|
||||
const item: CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
minHeight: 64,
|
||||
padding: '6px 4px',
|
||||
border: '2px solid transparent',
|
||||
borderRadius: 8,
|
||||
background: 'transparent',
|
||||
color: 'var(--text-primary)',
|
||||
font: 'inherit',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
overflowWrap: 'anywhere',
|
||||
cursor: 'grab',
|
||||
touchAction: 'none',
|
||||
};
|
||||
|
||||
export function DocumentationSidebar({ onItem }: DocumentationSidebarProps) {
|
||||
const t = useTranslate();
|
||||
const [ghost, setGhost] = useState<{ kind: DocumentationItemKind; x: number; y: number } | null>(null);
|
||||
const press = useRef<{ kind: DocumentationItemKind; x: number; y: number; dragging: boolean } | null>(null);
|
||||
/** The click the browser sends after a drag ends on the button: not a tap. */
|
||||
const afterDrag = useRef(false);
|
||||
|
||||
const onPointerDown = (kind: DocumentationItemKind) => (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
press.current = { kind, x: event.clientX, y: event.clientY, dragging: false };
|
||||
// The button is the click's own target: capturing at once takes no click from anything inside it.
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
};
|
||||
const onPointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
const now = press.current;
|
||||
if (now === null) return;
|
||||
if (!now.dragging) {
|
||||
if (Math.hypot(event.clientX - now.x, event.clientY - now.y) < DRAG_THRESHOLD) return;
|
||||
now.dragging = true;
|
||||
}
|
||||
setGhost({ kind: now.kind, x: event.clientX, y: event.clientY });
|
||||
};
|
||||
const onPointerUp = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
const now = press.current;
|
||||
press.current = null;
|
||||
setGhost(null);
|
||||
if (now?.dragging !== true) return;
|
||||
afterDrag.current = true;
|
||||
onItem(now.kind, { clientX: event.clientX, clientY: event.clientY });
|
||||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label={t('docu.sidebar')}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
padding: 6,
|
||||
overflowY: 'auto',
|
||||
background: 'var(--surface-card)',
|
||||
borderRight: '1px solid var(--border-default)',
|
||||
width: 124,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{ITEMS.map((each) => (
|
||||
<button
|
||||
key={each.kind}
|
||||
type="button"
|
||||
style={item}
|
||||
data-docu={each.kind}
|
||||
onPointerDown={onPointerDown(each.kind)}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={() => {
|
||||
press.current = null;
|
||||
setGhost(null);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (afterDrag.current) afterDrag.current = false;
|
||||
else onItem(each.kind, null);
|
||||
}}
|
||||
>
|
||||
<img src={each.icon} alt="" width={40} height={40} draggable={false} />
|
||||
{t(each.label)}
|
||||
</button>
|
||||
))}
|
||||
{ghost === null ? null : (
|
||||
// The item's own size, its middle under the pointer, as Java drags it.
|
||||
<div
|
||||
data-ghost
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: ghost.x,
|
||||
top: ghost.y,
|
||||
width: 100,
|
||||
height: 50,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
border: `1px ${ghost.kind === 'text' ? 'solid rgb(179, 179, 179)' : 'solid rgb(163, 163, 163)'}`,
|
||||
borderRadius: 5,
|
||||
background: ghost.kind === 'rectangle' ? 'rgba(204, 204, 204, 0.78)' : 'transparent',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user