mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
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.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import type { BookElement, BookSpread } from '@trek/shared';
|
||||
import { bookShapeElementSchema, bookTextElementSchema } from '@trek/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { dropOn, layerLabel, layerRows, rowIds } from './studioLayerRows';
|
||||
import { shortcutKeys } from './studioShortcut';
|
||||
|
||||
/** The value, or a failed test when it is missing. */
|
||||
function must<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error('expected a value');
|
||||
return value;
|
||||
}
|
||||
|
||||
const shape = (id: string, over: Partial<BookElement> = {}) =>
|
||||
bookShapeElementSchema.parse({
|
||||
id,
|
||||
kind: 'shape',
|
||||
frame: { x: 0, y: 0, w: 10, h: 10 },
|
||||
shape: 'rect',
|
||||
...over,
|
||||
}) as BookElement;
|
||||
|
||||
const spread = {
|
||||
id: 'sp',
|
||||
role: 'inner',
|
||||
background: null,
|
||||
parked: [],
|
||||
entryId: null,
|
||||
elements: [shape('back'), shape('m1', { group: 'g' }), shape('m2', { group: 'g' }), shape('front')],
|
||||
groups: [{ id: 'g', name: 'Pair' }],
|
||||
} as BookSpread;
|
||||
|
||||
describe('layerRows', () => {
|
||||
it('lists the page front to back, a group with its members under it', () => {
|
||||
const rows = layerRows(spread, new Set());
|
||||
expect(rows.map((r) => r.key)).toEqual(['front', 'g:g', 'm2', 'm1', 'back']);
|
||||
const group = must(rows[1]);
|
||||
expect(group.type === 'group' && group.name).toBe('Pair');
|
||||
expect(rowIds(group)).toEqual(['m2', 'm1']);
|
||||
expect(rowIds(must(rows[0]))).toEqual(['front']);
|
||||
});
|
||||
|
||||
it('shows a folded group as its row alone', () => {
|
||||
expect(layerRows(spread, new Set(['g'])).map((r) => r.key)).toEqual(['front', 'g:g', 'back']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropOn', () => {
|
||||
const rows = layerRows(spread, new Set());
|
||||
it('puts what is dragged in front of or behind an element row', () => {
|
||||
expect(dropOn(must(rows[0]), true)).toEqual({ anchorId: 'front', where: 'above', join: false });
|
||||
expect(dropOn(must(rows[4]), false)).toEqual({ anchorId: 'back', where: 'below', join: false });
|
||||
});
|
||||
|
||||
it('takes it into a group on a member row, and beside the whole group on the group row', () => {
|
||||
expect(dropOn(must(rows[2]), false)).toEqual({ anchorId: 'm2', where: 'below', join: true });
|
||||
expect(dropOn(must(rows[1]), true)).toEqual({ anchorId: 'm2', where: 'above', join: false });
|
||||
expect(dropOn(must(rows[1]), false)).toEqual({ anchorId: 'm1', where: 'below', join: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('layerLabel', () => {
|
||||
it('uses the name, then a text’s first words, then the kind', () => {
|
||||
expect(layerLabel(shape('a', { name: 'Sun' }), 'Shape')).toBe('Sun');
|
||||
expect(layerLabel(shape('a'), 'Shape')).toBe('Shape');
|
||||
const words = bookTextElementSchema.parse({
|
||||
id: 't',
|
||||
kind: 'text',
|
||||
frame: { x: 0, y: 0, w: 1, h: 1 },
|
||||
text: '\n Reykjavik \nmore',
|
||||
});
|
||||
expect(layerLabel(words as BookElement, 'Text')).toBe('Reykjavik');
|
||||
const long = bookTextElementSchema.parse({
|
||||
id: 't',
|
||||
kind: 'text',
|
||||
frame: { x: 0, y: 0, w: 1, h: 1 },
|
||||
text: 'x'.repeat(60),
|
||||
});
|
||||
expect(layerLabel(long as BookElement, 'Text')).toHaveLength(40);
|
||||
const empty = bookTextElementSchema.parse({ id: 't', kind: 'text', frame: { x: 0, y: 0, w: 1, h: 1 }, text: ' ' });
|
||||
expect(layerLabel(empty as BookElement, 'Text')).toBe('Text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortcutKeys', () => {
|
||||
it('spells the command and option keys for the platform', () => {
|
||||
expect(shortcutKeys(true, 'Ctrl', ['⇧', 'G'])).toEqual(['⌘', '⇧', 'G']);
|
||||
expect(shortcutKeys(false, 'Strg', ['alt', 'C'])).toEqual(['Strg', 'Alt', 'C']);
|
||||
expect(shortcutKeys(true, 'Ctrl', ['alt', 'V'])).toEqual(['⌘', '⌥', 'V']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user