Files
TREK/client/src/components/Studio/StudioSelectionChrome.tsx
T
Maurice d8213bfa42 feat(studio): groups, a layers panel, marquee, nudge, multi-resize, align and paste style
Elements can be grouped on a page and picked, moved, resized, aligned and
restacked as one. A layers panel lists the page front to back with lock,
visibility, rename and drag to restack. The sheet gains marquee selection,
arrow-key nudge, one box that resizes a multi-selection with Shift holding
the proportions, and the design-tool shortcuts. The inspector lines a
selection up and spaces it out, and copies and pastes a style.

The assistant gets the same through the group, ungroup, name and hide
operations, and describe_book_page lists the groups. The format gains the
optional group, name and hidden fields and a spread's group names, so
BOOK_SCHEMA_REV goes to 2.
2026-10-23 21:07:13 +02:00

61 lines
2.1 KiB
TypeScript

import type { BookFrame } from '@trek/shared';
import { HANDLES, HANDLE_POS, type HandleId } from './studioGestures';
/**
* The selection chrome the sheet draws above the page for more than one
* element: one box with the eight resize handles around all of them, dashed
* when it is exactly one group, and the marquee while one is being drawn.
* Screen pixels, like every outline on the sheet, so a line stays a hairline
* at any zoom. The sheet is laid out in millimetres from its left edge in
* every reading direction, which is why these sit at physical offsets.
*/
/** The rectangle of a frame on screen. */
const onScreen = (f: BookFrame, scaled: number) => ({
left: f.x * scaled, // rtl-lint-disable: the sheet is measured from its left edge
top: f.y * scaled,
width: f.w * scaled,
height: f.h * scaled,
});
/** The eight resize handles of a box, corners round and edges as short bars. */
export function ResizeHandles({ onResize }: { onResize: (e: React.PointerEvent, handle: HandleId) => void }) {
return (
<>
{HANDLES.map((h) => {
const kind = h.length === 2 ? 'is-corner' : h === 'n' || h === 's' ? 'is-h' : 'is-v';
const at = HANDLE_POS[h];
const place = { left: at.x, top: at.y, cursor: at.cursor }; // rtl-lint-disable: a corner of the box, not a side of the text
return <span key={h} className={`st-handle ${kind}`} style={place} onPointerDown={(e) => onResize(e, h)} />;
})}
</>
);
}
/** One box around a multi-selection, with handles that resize all of it (Shift keeps the proportions). */
export function SelectionBox({
box,
scaled,
asGroup,
handles,
onResize,
}: {
box: BookFrame;
scaled: number;
asGroup: boolean;
handles: boolean;
onResize: (e: React.PointerEvent, handle: HandleId) => void;
}) {
return (
<div className={`st-select-box ${asGroup ? 'is-group' : ''}`} style={onScreen(box, scaled)}>
{handles && <ResizeHandles onResize={onResize} />}
</div>
);
}
/** The marquee being drawn. */
export function MarqueeRect({ rect, scaled }: { rect: BookFrame; scaled: number }) {
return <div className="st-marquee" style={onScreen(rect, scaled)} />;
}