mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 08:11:03 +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,146 @@
|
||||
import {
|
||||
alignElements,
|
||||
arrangeElements,
|
||||
arrangeWithinGroup,
|
||||
boundsOfFrames,
|
||||
distributableCount,
|
||||
distributeElements,
|
||||
} from './book-arrange';
|
||||
import {
|
||||
bookPageSetupSchema,
|
||||
bookShapeElementSchema,
|
||||
bookSpreadSchema,
|
||||
type BookElement,
|
||||
type BookSpread,
|
||||
} from './book.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Paint order, alignment and spacing, with groups moving as one. The page is
|
||||
* the default square book: 210 mm pages, the fold at 210, 5 mm safe margin.
|
||||
*/
|
||||
|
||||
const pageSetup = bookPageSetupSchema.parse({});
|
||||
|
||||
const box = (id: string, x: number, y: number, w = 20, h = 20, over: Partial<BookElement> = {}) =>
|
||||
bookShapeElementSchema.parse({ id, kind: 'shape', frame: { x, y, w, h }, shape: 'rect', ...over }) as BookElement;
|
||||
|
||||
const spread = (elements: BookElement[]): BookSpread => bookSpreadSchema.parse({ id: 'sp', elements });
|
||||
const ids = (sp: BookSpread) => sp.elements.map((e) => e.id);
|
||||
const at = (sp: BookSpread, id: string) => sp.elements.find((e) => e.id === id)?.frame;
|
||||
const pick = (sp: BookSpread, list: string[]) => sp.elements.filter((e) => list.includes(e.id));
|
||||
|
||||
describe('paint order', () => {
|
||||
const sp = spread([
|
||||
box('a', 0, 0),
|
||||
box('g1', 0, 0, 20, 20, { group: 'g' }),
|
||||
box('g2', 0, 0, 20, 20, { group: 'g' }),
|
||||
box('b', 0, 0),
|
||||
]);
|
||||
|
||||
it('BKARR-001: to front and to back keep the moved ones in their order, and a group together', () => {
|
||||
expect(ids(arrangeElements(sp, ['a'], 'front'))).toEqual(['g1', 'g2', 'b', 'a']);
|
||||
expect(ids(arrangeElements(sp, ['b'], 'back'))).toEqual(['b', 'a', 'g1', 'g2']);
|
||||
// One member moved to the front takes its group along.
|
||||
expect(ids(arrangeElements(sp, ['g1'], 'front'))).toEqual(['a', 'b', 'g1', 'g2']);
|
||||
});
|
||||
|
||||
it('BKARR-002: forward and backward step one place', () => {
|
||||
// A step goes past a whole group, never into it.
|
||||
expect(ids(arrangeElements(sp, ['a'], 'forward'))).toEqual(['g1', 'g2', 'a', 'b']);
|
||||
expect(ids(arrangeElements(sp, ['g2'], 'backward'))).toEqual(['g1', 'g2', 'a', 'b']);
|
||||
expect(arrangeElements(sp, ['b'], 'front')).toBe(sp);
|
||||
expect(ids(arrangeElements(spread([box('a', 0, 0), box('b', 0, 0), box('c', 0, 0)]), ['a'], 'forward'))).toEqual([
|
||||
'b',
|
||||
'a',
|
||||
'c',
|
||||
]);
|
||||
expect(ids(arrangeElements(spread([box('a', 0, 0), box('b', 0, 0), box('c', 0, 0)]), ['c'], 'backward'))).toEqual([
|
||||
'a',
|
||||
'c',
|
||||
'b',
|
||||
]);
|
||||
});
|
||||
|
||||
it('BKARR-003: inside a group, a member moves among the members only', () => {
|
||||
const grouped = spread([
|
||||
box('a', 0, 0),
|
||||
box('m1', 0, 0, 20, 20, { group: 'g' }),
|
||||
box('m2', 0, 0, 20, 20, { group: 'g' }),
|
||||
box('m3', 0, 0, 20, 20, { group: 'g' }),
|
||||
box('b', 0, 0),
|
||||
]);
|
||||
expect(ids(arrangeWithinGroup(grouped, 'g', ['m1'], 'front'))).toEqual(['a', 'm2', 'm3', 'm1', 'b']);
|
||||
expect(ids(arrangeWithinGroup(grouped, 'g', ['m3'], 'back'))).toEqual(['a', 'm3', 'm1', 'm2', 'b']);
|
||||
expect(arrangeWithinGroup(grouped, 'g', ['m3'], 'front')).toBe(grouped);
|
||||
expect(arrangeWithinGroup(grouped, 'g', ['m3'], 'front').elements[3]?.group).toBe('g');
|
||||
});
|
||||
});
|
||||
|
||||
describe('align and distribute', () => {
|
||||
it('BKARR-004: lines single elements up on the edge of their box', () => {
|
||||
const sp = spread([box('a', 10, 10), box('b', 40, 30)]);
|
||||
const left = alignElements(sp, pageSetup, sp.elements, 'left', 'selection');
|
||||
expect([at(left, 'a')?.x, at(left, 'b')?.x]).toEqual([10, 10]);
|
||||
const bottom = alignElements(sp, pageSetup, sp.elements, 'bottom', 'selection');
|
||||
expect([at(bottom, 'a')?.y, at(bottom, 'b')?.y]).toEqual([30, 30]);
|
||||
// The other axis is left exactly as it was.
|
||||
expect(at(left, 'b')?.y).toBe(30);
|
||||
});
|
||||
|
||||
it('BKARR-005: moves a group as one box, and lines a group up inside itself when it is all that is picked', () => {
|
||||
const sp = spread([
|
||||
box('a', 100, 10),
|
||||
box('g1', 10, 50, 20, 20, { group: 'g' }),
|
||||
box('g2', 40, 60, 20, 20, { group: 'g' }),
|
||||
]);
|
||||
const right = alignElements(sp, pageSetup, sp.elements, 'right', 'selection');
|
||||
// The group's box (10..60) goes to the right edge 120 as one: 60 to the right.
|
||||
expect([at(right, 'g1')?.x, at(right, 'g2')?.x, at(right, 'a')?.x]).toEqual([70, 100, 100]);
|
||||
const inside = alignElements(sp, pageSetup, pick(sp, ['g1', 'g2']), 'top', 'selection');
|
||||
expect([at(inside, 'g1')?.y, at(inside, 'g2')?.y]).toEqual([50, 50]);
|
||||
});
|
||||
|
||||
it('BKARR-006: aligns against the page under an element, and its safe area', () => {
|
||||
const sp = spread([box('r', pageSetup.pageWidth + 90, 10)]);
|
||||
const centred = alignElements(sp, pageSetup, sp.elements, 'center', 'page');
|
||||
expect(at(centred, 'r')?.x).toBe(pageSetup.pageWidth + (pageSetup.pageWidth - 20) / 2);
|
||||
const safe = alignElements(sp, pageSetup, sp.elements, 'top', 'safe');
|
||||
expect(at(safe, 'r')?.y).toBe(pageSetup.safe);
|
||||
const middle = alignElements(sp, pageSetup, sp.elements, 'middle', 'page');
|
||||
expect(at(middle, 'r')?.y).toBe((pageSetup.pageHeight - 20) / 2);
|
||||
});
|
||||
|
||||
it('BKARR-007: spaces units evenly, a group counted once, or by a fixed gap', () => {
|
||||
const sp = spread([
|
||||
box('a', 0, 0),
|
||||
box('g1', 30, 0, 10, 10, { group: 'g' }),
|
||||
box('g2', 45, 0, 10, 10, { group: 'g' }),
|
||||
box('c', 100, 0),
|
||||
]);
|
||||
expect(distributableCount(sp, sp.elements)).toBe(3);
|
||||
const even = distributeElements(sp, sp.elements, 'horizontal');
|
||||
// Span 0..120, sizes 20 + 25 + 20, so two gaps of 27.5; the group moves as one.
|
||||
expect([at(even, 'g1')?.x, at(even, 'g2')?.x, at(even, 'c')?.x]).toEqual([47.5, 62.5, 100]);
|
||||
const fixed = distributeElements(sp, sp.elements, 'horizontal', 5);
|
||||
expect([at(fixed, 'g1')?.x, at(fixed, 'c')?.x]).toEqual([25, 55]);
|
||||
const down = distributeElements(
|
||||
spread([box('a', 0, 0), box('b', 0, 50), box('c', 0, 30)]),
|
||||
spread([box('a', 0, 0), box('b', 0, 50), box('c', 0, 30)]).elements,
|
||||
'vertical',
|
||||
);
|
||||
expect(at(down, 'c')?.y).toBe(25);
|
||||
});
|
||||
|
||||
it('BKARR-008: leaves a page with fewer than three units as it is, and measures a box', () => {
|
||||
const sp = spread([box('a', 0, 0), box('b', 50, 0)]);
|
||||
expect(distributeElements(sp, sp.elements, 'horizontal')).toBe(sp);
|
||||
expect(
|
||||
boundsOfFrames([
|
||||
{ x: 1, y: 2, w: 3, h: 4 },
|
||||
{ x: 10, y: 0, w: 1, h: 1 },
|
||||
]),
|
||||
).toEqual({ x: 1, y: 0, w: 10, h: 6 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import { commonGroup, gatherGroups } from './book-groups';
|
||||
import type { BookElement, BookFrame, BookPageSetup, BookSpread } from './book.schema';
|
||||
|
||||
/**
|
||||
* Paint order, alignment and spacing of the elements of a page.
|
||||
*
|
||||
* The `arrange`, `align` and `distribute` operations of the book engine run
|
||||
* these (book-ops-elements.ts), and the Studio's layer buttons, align bar and
|
||||
* layers panel run the same engine, so a press and an assistant's operation
|
||||
* move a page the same way.
|
||||
*
|
||||
* Groups move as one: aligning two groups lines up their boxes, not every
|
||||
* member on the same edge, and distributing spaces the boxes. When every named
|
||||
* element is in the one group, the members line up among themselves, which is
|
||||
* how the inside of a lockup is set.
|
||||
*/
|
||||
|
||||
export type BookArrangeTo = 'front' | 'forward' | 'backward' | 'back';
|
||||
export type BookAlignEdge = 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom';
|
||||
export type BookAlignTo = 'selection' | 'page' | 'safe';
|
||||
export type BookDistributeAxis = 'horizontal' | 'vertical';
|
||||
|
||||
const round2 = (v: number) => Math.round(v * 100) / 100;
|
||||
|
||||
/**
|
||||
* The page in paint order as the things that move as one: a group's members
|
||||
* together (they are next to each other, see `gatherGroups`), every other
|
||||
* element alone.
|
||||
*/
|
||||
function paintUnits(spread: BookSpread): BookElement[][] {
|
||||
const units: BookElement[][] = [];
|
||||
for (const el of gatherGroups(spread).elements) {
|
||||
const last = units[units.length - 1];
|
||||
if (el.group && last?.[0]?.group === el.group) last.push(el);
|
||||
else units.push([el]);
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
/**
|
||||
* The named elements moved in the paint order, keeping their order among
|
||||
* themselves. A group moves whole, and a step forward or backward goes past a
|
||||
* whole group at once, never into it.
|
||||
*/
|
||||
export function arrangeElements(spread: BookSpread, ids: readonly string[], to: BookArrangeTo): BookSpread {
|
||||
const chosen = new Set(ids);
|
||||
const units = paintUnits(spread);
|
||||
const isChosen = (u: BookElement[]) => u.some((e) => chosen.has(e.id));
|
||||
let next: BookElement[][];
|
||||
if (to === 'front') next = [...units.filter((u) => !isChosen(u)), ...units.filter(isChosen)];
|
||||
else if (to === 'back') next = [...units.filter(isChosen), ...units.filter((u) => !isChosen(u))];
|
||||
else {
|
||||
next = units.slice();
|
||||
const step = to === 'forward' ? 1 : -1;
|
||||
const order = next.map((_, i) => i);
|
||||
if (step === 1) order.reverse();
|
||||
for (const i of order) {
|
||||
const here = next[i];
|
||||
const there = next[i + step];
|
||||
if (!here || !there || !isChosen(here) || isChosen(there)) continue;
|
||||
next[i] = there;
|
||||
next[i + step] = here;
|
||||
}
|
||||
}
|
||||
const elements = next.flat();
|
||||
return elements.every((e, i) => e === spread.elements[i]) ? spread : { ...spread, elements };
|
||||
}
|
||||
|
||||
/**
|
||||
* Members of one group moved in the paint order among the group's own members
|
||||
* only, the group staying where it is on the page: what "to front" does to an
|
||||
* element picked inside a group.
|
||||
*/
|
||||
export function arrangeWithinGroup(
|
||||
spread: BookSpread,
|
||||
groupId: string,
|
||||
ids: readonly string[],
|
||||
to: BookArrangeTo,
|
||||
): BookSpread {
|
||||
const slots: number[] = [];
|
||||
spread.elements.forEach((e, i) => {
|
||||
if (e.group === groupId) slots.push(i);
|
||||
});
|
||||
const members = slots.map((i) => spread.elements[i] as BookElement);
|
||||
const reordered = arrangeElements({ ...spread, elements: members.map(withoutGroupId) }, ids, to).elements;
|
||||
const byId = new Map(members.map((m) => [m.id, m]));
|
||||
const elements = spread.elements.slice();
|
||||
slots.forEach((slot, k) => {
|
||||
const moved = reordered[k];
|
||||
if (moved) elements[slot] = byId.get(moved.id) as BookElement;
|
||||
});
|
||||
return elements.every((e, i) => e === spread.elements[i]) ? spread : { ...spread, elements };
|
||||
}
|
||||
|
||||
/** A member taken out of its group for reordering among the members alone, so gathering leaves them be. */
|
||||
function withoutGroupId(el: BookElement): BookElement {
|
||||
const next = { ...el };
|
||||
delete next.group;
|
||||
return next;
|
||||
}
|
||||
|
||||
/** The box around frames. */
|
||||
export function boundsOfFrames(frames: readonly BookFrame[]): BookFrame {
|
||||
const x = Math.min(...frames.map((f) => f.x));
|
||||
const y = Math.min(...frames.map((f) => f.y));
|
||||
const right = Math.max(...frames.map((f) => f.x + f.w));
|
||||
const bottom = Math.max(...frames.map((f) => f.y + f.h));
|
||||
return { x, y, w: right - x, h: bottom - y };
|
||||
}
|
||||
|
||||
/** The page a box's centre is on, or its safe area. */
|
||||
function pageBox(spread: BookSpread, page: BookPageSetup, f: BookFrame, safe: boolean): BookFrame {
|
||||
const onRight = spread.role === 'inner' && f.x + f.w / 2 >= page.pageWidth;
|
||||
const x = onRight ? page.pageWidth : 0;
|
||||
const inset = safe ? page.safe : 0;
|
||||
return { x: x + inset, y: inset, w: page.pageWidth - inset * 2, h: page.pageHeight - inset * 2 };
|
||||
}
|
||||
|
||||
/** Where a box goes to line up on an edge of another. */
|
||||
function alignedAt(f: BookFrame, box: BookFrame, edge: BookAlignEdge): { x: number; y: number } {
|
||||
switch (edge) {
|
||||
case 'left':
|
||||
return { x: box.x, y: f.y };
|
||||
case 'center':
|
||||
return { x: box.x + (box.w - f.w) / 2, y: f.y };
|
||||
case 'right':
|
||||
return { x: box.x + box.w - f.w, y: f.y };
|
||||
case 'top':
|
||||
return { x: f.x, y: box.y };
|
||||
case 'middle':
|
||||
return { x: f.x, y: box.y + (box.h - f.h) / 2 };
|
||||
case 'bottom':
|
||||
return { x: f.x, y: box.y + box.h - f.h };
|
||||
}
|
||||
}
|
||||
|
||||
/** What moves as one: a group's named members together, every other element alone. */
|
||||
interface Unit {
|
||||
members: BookElement[];
|
||||
box: BookFrame;
|
||||
}
|
||||
|
||||
function unitsOf(spread: BookSpread, els: readonly BookElement[]): Unit[] {
|
||||
const ids = els.map((e) => e.id);
|
||||
// All in one group: the members are the units, so the inside of a group can be set.
|
||||
const inside = commonGroup(spread, ids) !== null;
|
||||
const byKey = new Map<string, BookElement[]>();
|
||||
for (const el of els) {
|
||||
const key = !inside && el.group ? `g:${el.group}` : `e:${el.id}`;
|
||||
byKey.set(key, [...(byKey.get(key) ?? []), el]);
|
||||
}
|
||||
return [...byKey.values()].map((members) => ({ members, box: boundsOfFrames(members.map((m) => m.frame)) }));
|
||||
}
|
||||
|
||||
/** Every member of a unit moved by the same offset, rounded as the document stores it. */
|
||||
function shifted(unit: Unit, dx: number, dy: number): BookElement[] {
|
||||
// An axis that does not move is left exactly as it was, unrounded or not.
|
||||
return unit.members.map((e) => {
|
||||
const x = dx === 0 ? e.frame.x : round2(e.frame.x + dx);
|
||||
const y = dy === 0 ? e.frame.y : round2(e.frame.y + dy);
|
||||
return { ...e, frame: { ...e.frame, x, y } } as BookElement;
|
||||
});
|
||||
}
|
||||
|
||||
function replaceAll(spread: BookSpread, next: readonly BookElement[]): BookSpread {
|
||||
const byId = new Map(next.map((e) => [e.id, e]));
|
||||
return { ...spread, elements: spread.elements.map((e) => byId.get(e.id) ?? e) };
|
||||
}
|
||||
|
||||
/** The named elements lined up on an edge or a centre line of their box, their page, or its safe area. */
|
||||
export function alignElements(
|
||||
spread: BookSpread,
|
||||
page: BookPageSetup,
|
||||
els: readonly BookElement[],
|
||||
edge: BookAlignEdge,
|
||||
to: BookAlignTo,
|
||||
): BookSpread {
|
||||
const units = unitsOf(spread, els);
|
||||
const selection = boundsOfFrames(units.map((u) => u.box));
|
||||
const next = units.flatMap((u) => {
|
||||
const box = to === 'selection' ? selection : pageBox(spread, page, u.box, to === 'safe');
|
||||
const at = alignedAt(u.box, box, edge);
|
||||
return shifted(u, at.x - u.box.x, at.y - u.box.y);
|
||||
});
|
||||
return replaceAll(spread, next);
|
||||
}
|
||||
|
||||
/** How many units a distribute has: it needs three. */
|
||||
export function distributableCount(spread: BookSpread, els: readonly BookElement[]): number {
|
||||
return unitsOf(spread, els).length;
|
||||
}
|
||||
|
||||
/** Three or more units spaced evenly between the outer two, or by a fixed gap in millimetres. */
|
||||
export function distributeElements(
|
||||
spread: BookSpread,
|
||||
els: readonly BookElement[],
|
||||
axis: BookDistributeAxis,
|
||||
gap?: number,
|
||||
): BookSpread {
|
||||
const units = unitsOf(spread, els);
|
||||
if (units.length < 3) return spread;
|
||||
const across = axis === 'horizontal';
|
||||
const start = (f: BookFrame) => (across ? f.x : f.y);
|
||||
const size = (f: BookFrame) => (across ? f.w : f.h);
|
||||
const sorted = units.slice().sort((a, b) => start(a.box) - start(b.box));
|
||||
const first = sorted[0] as Unit;
|
||||
const last = sorted[sorted.length - 1] as Unit;
|
||||
const total = sorted.reduce((sum, u) => sum + size(u.box), 0);
|
||||
const span = start(last.box) + size(last.box) - start(first.box);
|
||||
const space = gap ?? (span - total) / (sorted.length - 1);
|
||||
let cursor = start(first.box);
|
||||
const next = sorted.flatMap((u) => {
|
||||
const delta = round2(cursor) - start(u.box);
|
||||
cursor += size(u.box) + space;
|
||||
return across ? shifted(u, delta, 0) : shifted(u, 0, delta);
|
||||
});
|
||||
return replaceAll(spread, next);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { compareCodeUnits } from '../common/compare';
|
||||
import { contrastOfLuminance, deltaE, greyLuminance, luminance, parseHex, requiredContrast } from './book-colour';
|
||||
import { bookFontCountsInMix, bookFontsUsed } from './book-fonts';
|
||||
import { visibleSpread } from './book-groups';
|
||||
import type { BookPhotoInfo } from './book-ops-core';
|
||||
import { BOOK_OP_LIMITS, type BookFinding } from './book-ops.schema';
|
||||
import type { BookDocument, BookElement, BookFrame, BookPageSetup, BookPhotoElement, BookSpread } from './book.schema';
|
||||
@@ -300,7 +301,7 @@ export function designFindings(
|
||||
page: BookPageSetup,
|
||||
photos?: readonly BookPhotoInfo[],
|
||||
): BookFinding[] {
|
||||
const ctx: Ctx = { spread, page, photos, out: [] };
|
||||
const ctx: Ctx = { spread: visibleSpread(spread), page, photos, out: [] };
|
||||
contrastAndBusy(ctx);
|
||||
smallText(ctx);
|
||||
crowded(ctx);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bookVisiblePart, sortBookFindings } from './book-findings-design';
|
||||
import { bookFontFace } from './book-fonts';
|
||||
import { stickerFindings } from './book-findings-sticker';
|
||||
import { visibleSpread } from './book-groups';
|
||||
import type { BookPhotoInfo } from './book-ops-core';
|
||||
import type { BookFinding } from './book-ops.schema';
|
||||
import type { BookElement, BookPageSetup, BookPhotoElement, BookSpread, BookTextElement } from './book.schema';
|
||||
@@ -242,6 +243,8 @@ function overlaps(ctx: Ctx): void {
|
||||
const a = content[i] as BookElement;
|
||||
const b = content[j] as BookElement;
|
||||
if (a.kind !== b.kind) continue;
|
||||
// Set over each other inside one group on purpose: a lockup, a framed caption.
|
||||
if (a.group !== undefined && a.group === b.group) continue;
|
||||
const ba = elementBox(a);
|
||||
const bb = elementBox(b);
|
||||
const shared = overlapArea(ba, bb);
|
||||
@@ -487,7 +490,8 @@ export function spreadFindings(
|
||||
photos?: readonly BookPhotoInfo[],
|
||||
): BookFinding[] {
|
||||
const ctx: Ctx = {
|
||||
spread,
|
||||
// A hidden element is not on the page, so nothing about it is a fault of the page.
|
||||
spread: visibleSpread(spread),
|
||||
page,
|
||||
W: page.pageWidth * (spread.role === 'inner' ? 2 : 1),
|
||||
H: page.pageHeight,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { bookElementIdSchema } from './book-op-ids';
|
||||
import { BOOK_LAYER_NAME_MAX } from './book.schema';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* The layer operations of a book page: group, ungroup, name and hide. They
|
||||
* join the page operations of book-ops.schema.ts beside the element ones, run
|
||||
* in the one engine (book-ops-groups.ts) the Studio's layers panel and its
|
||||
* Ctrl+G use too.
|
||||
*/
|
||||
|
||||
/** A group's id as the page has it (`g-...`), never invented. */
|
||||
export const bookGroupIdSchema = z.string().regex(/^[\w-]{1,40}$/);
|
||||
|
||||
const layerName = z.string().max(BOOK_LAYER_NAME_MAX);
|
||||
|
||||
export const BOOK_GROUP_OP_SCHEMAS = [
|
||||
/**
|
||||
* Make two or more elements one group, picked, moved, aligned and arranged
|
||||
* as one. Elements already in a group leave it for the new one.
|
||||
*/
|
||||
z.strictObject({
|
||||
op: z.literal('group'),
|
||||
elementIds: z.array(bookElementIdSchema).min(2).max(30),
|
||||
name: layerName
|
||||
.describe('What the group is called in the layers panel, such as "Title lockup". Optional.')
|
||||
.optional(),
|
||||
}),
|
||||
/** Put a group's members back on their own, where they are. */
|
||||
z.strictObject({ op: z.literal('ungroup'), groupId: bookGroupIdSchema }),
|
||||
/** Name an element or a group in the layers panel; an empty name clears it. */
|
||||
z
|
||||
.strictObject({
|
||||
op: z.literal('name'),
|
||||
elementId: bookElementIdSchema.optional(),
|
||||
groupId: bookGroupIdSchema.optional(),
|
||||
name: layerName,
|
||||
})
|
||||
.refine((v) => (v.elementId === undefined) !== (v.groupId === undefined), {
|
||||
message: 'name an elementId or a groupId, not both',
|
||||
}),
|
||||
/** Hide elements (kept on the page's layer list, left out of the page and its print) or show them again. */
|
||||
z.strictObject({
|
||||
op: z.literal('hide'),
|
||||
elementIds: z.array(bookElementIdSchema).min(1).max(30),
|
||||
hidden: z.boolean(),
|
||||
}),
|
||||
] as const;
|
||||
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
bookGroupName,
|
||||
commonGroup,
|
||||
expandToGroups,
|
||||
gatherGroups,
|
||||
groupElements,
|
||||
groupMembers,
|
||||
isWholeGroup,
|
||||
regroupCopies,
|
||||
renameElement,
|
||||
renameGroup,
|
||||
setElementsHidden,
|
||||
spreadGroupIds,
|
||||
tidyGroups,
|
||||
ungroupElements,
|
||||
visibleSpread,
|
||||
withCopies,
|
||||
withoutGroup,
|
||||
} from './book-groups';
|
||||
import { bookShapeElementSchema, bookSpreadSchema, type BookElement, type BookSpread } from './book.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Groups on a page: membership on the elements, names on the spread, at least
|
||||
* two members each, kept next to each other in the paint order.
|
||||
*/
|
||||
|
||||
const shape = (id: string, over: Partial<BookElement> = {}) =>
|
||||
bookShapeElementSchema.parse({
|
||||
id,
|
||||
kind: 'shape',
|
||||
frame: { x: 10, y: 10, w: 20, h: 20 },
|
||||
shape: 'rect',
|
||||
...over,
|
||||
}) as BookElement;
|
||||
|
||||
const page = (elements: BookElement[], over: Partial<BookSpread> = {}): BookSpread =>
|
||||
bookSpreadSchema.parse({ id: 'sp', elements, ...over });
|
||||
|
||||
const order = (sp: BookSpread) => sp.elements.map((e) => `${e.id}${e.group ? `:${e.group}` : ''}`);
|
||||
|
||||
describe('reading groups', () => {
|
||||
const sp = page(
|
||||
[
|
||||
shape('a', { group: 'g1' }),
|
||||
shape('b'),
|
||||
shape('c', { group: 'g1' }),
|
||||
shape('d', { group: 'g2' }),
|
||||
shape('e', { group: 'g2' }),
|
||||
],
|
||||
{ groups: [{ id: 'g1', name: 'Lockup' }] },
|
||||
);
|
||||
|
||||
it('BKGRP-001: lists members, names and the groups on the page in paint order', () => {
|
||||
expect(groupMembers(sp, 'g1').map((e) => e.id)).toEqual(['a', 'c']);
|
||||
expect(bookGroupName(sp, 'g1')).toBe('Lockup');
|
||||
expect(bookGroupName(sp, 'g2')).toBeUndefined();
|
||||
expect(spreadGroupIds(sp)).toEqual(['g1', 'g2']);
|
||||
});
|
||||
|
||||
it('BKGRP-002: a click on one member picks the whole group, and an ungrouped element alone', () => {
|
||||
expect(expandToGroups(sp, ['c'])).toEqual(['a', 'c']);
|
||||
expect(expandToGroups(sp, ['b'])).toEqual(['b']);
|
||||
expect(expandToGroups(sp, ['b', 'e'])).toEqual(['b', 'd', 'e']);
|
||||
});
|
||||
|
||||
it('BKGRP-003: knows the one group a selection is in, and whether it is the whole of it', () => {
|
||||
expect(commonGroup(sp, ['a', 'c'])).toBe('g1');
|
||||
expect(commonGroup(sp, ['a', 'd'])).toBeNull();
|
||||
expect(commonGroup(sp, ['b'])).toBeNull();
|
||||
expect(commonGroup(sp, [])).toBeNull();
|
||||
expect(isWholeGroup(sp, ['a', 'c'])).toBe(true);
|
||||
expect(isWholeGroup(sp, ['a'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('making and undoing groups', () => {
|
||||
it('BKGRP-004: gathers a group at the place of its topmost member, keeping their order', () => {
|
||||
const sp = page([shape('a', { group: 'g' }), shape('b'), shape('c', { group: 'g' }), shape('d')]);
|
||||
expect(order(gatherGroups(sp))).toEqual(['b', 'a:g', 'c:g', 'd']);
|
||||
const tidy = page([shape('a', { group: 'g' }), shape('c', { group: 'g' }), shape('d')]);
|
||||
expect(gatherGroups(tidy)).toBe(tidy);
|
||||
const none = page([shape('a')]);
|
||||
expect(gatherGroups(none)).toBe(none);
|
||||
});
|
||||
|
||||
it('BKGRP-005: groups elements under a new id and a trimmed name, gathered together', () => {
|
||||
const sp = groupElements(page([shape('a'), shape('b'), shape('c')]), ['a', 'c'], 'g-new', ' Title ');
|
||||
expect(order(sp)).toEqual(['b', 'a:g-new', 'c:g-new']);
|
||||
expect(sp.groups).toEqual([{ id: 'g-new', name: 'Title' }]);
|
||||
const unnamed = groupElements(page([shape('a'), shape('b')]), ['a', 'b'], 'g2');
|
||||
expect(unnamed.groups).toBeUndefined();
|
||||
});
|
||||
|
||||
it('BKGRP-006: an element taken into a new group leaves its old one, which dissolves when one is left', () => {
|
||||
const sp = page([shape('a', { group: 'old' }), shape('b', { group: 'old' }), shape('c')], {
|
||||
groups: [{ id: 'old', name: 'Old' }],
|
||||
});
|
||||
const next = groupElements(sp, ['b', 'c'], 'new');
|
||||
expect(order(next)).toEqual(['a', 'b:new', 'c:new']);
|
||||
expect(next.groups).toBeUndefined();
|
||||
});
|
||||
|
||||
it('BKGRP-007: ungroups, leaving the members where they are and the name gone', () => {
|
||||
const sp = page([shape('a', { group: 'g' }), shape('b', { group: 'g' }), shape('c')], {
|
||||
groups: [{ id: 'g', name: 'X' }],
|
||||
});
|
||||
const next = ungroupElements(sp, 'g');
|
||||
expect(order(next)).toEqual(['a', 'b', 'c']);
|
||||
expect(next.groups).toBeUndefined();
|
||||
expect('group' in (next.elements[0] as object)).toBe(false);
|
||||
});
|
||||
|
||||
it('BKGRP-008: tidies a lone member and a name without a group, and leaves an orderly page as it is', () => {
|
||||
const sp = page([shape('a', { group: 'g' }), shape('b')], {
|
||||
groups: [
|
||||
{ id: 'g', name: 'Lone' },
|
||||
{ id: 'gone', name: 'Gone' },
|
||||
],
|
||||
});
|
||||
const next = tidyGroups(sp);
|
||||
expect(order(next)).toEqual(['a', 'b']);
|
||||
expect(next.groups).toBeUndefined();
|
||||
const fine = page([shape('a', { group: 'g' }), shape('b', { group: 'g' })], { groups: [{ id: 'g', name: 'Y' }] });
|
||||
expect(tidyGroups(fine)).toBe(fine);
|
||||
const keeps = tidyGroups(
|
||||
page([shape('a', { group: 'g' }), shape('b', { group: 'g' })], {
|
||||
groups: [
|
||||
{ id: 'g', name: 'Y' },
|
||||
{ id: 'x', name: 'Z' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(keeps.groups).toEqual([{ id: 'g', name: 'Y' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('layer names and visibility', () => {
|
||||
it('BKGRP-009: names an element or a group, and an empty name clears it', () => {
|
||||
const sp = page([shape('a', { group: 'g' }), shape('b', { group: 'g' })]);
|
||||
const named = renameElement(sp, 'a', ' Sun ');
|
||||
expect(named.elements[0]?.name).toBe('Sun');
|
||||
expect('name' in (renameElement(named, 'a', ' ').elements[0] as object)).toBe(false);
|
||||
const group = renameGroup(sp, 'g', 'Sky');
|
||||
expect(group.groups).toEqual([{ id: 'g', name: 'Sky' }]);
|
||||
expect(renameGroup(renameGroup(group, 'g', 'Sea'), 'g', '').groups).toBeUndefined();
|
||||
expect(renameGroup(group, 'g', 'Sea').groups).toEqual([{ id: 'g', name: 'Sea' }]);
|
||||
});
|
||||
|
||||
it('BKGRP-010: hides and shows, a shown element storing no flag, and the print leaves the hidden out', () => {
|
||||
const sp = page([shape('a'), shape('b')]);
|
||||
const hidden = setElementsHidden(sp, ['a'], true);
|
||||
expect(hidden.elements[0]?.hidden).toBe(true);
|
||||
expect(visibleSpread(hidden).elements.map((e) => e.id)).toEqual(['b']);
|
||||
expect(visibleSpread(sp)).toBe(sp);
|
||||
const shown = setElementsHidden(hidden, ['a'], false);
|
||||
expect('hidden' in (shown.elements[0] as object)).toBe(false);
|
||||
});
|
||||
|
||||
it('BKGRP-011: a document from before groups reads with none of the new fields set', () => {
|
||||
const sp = page([shape('a')]);
|
||||
expect(sp.groups).toBeUndefined();
|
||||
expect(Object.keys(sp.elements[0] as object)).not.toEqual(expect.arrayContaining(['group']));
|
||||
// A broken name list is dropped, never the page.
|
||||
const odd = bookSpreadSchema.parse({ id: 'sp', elements: [], groups: 'nonsense' });
|
||||
expect(odd.groups).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('copies', () => {
|
||||
let n = 0;
|
||||
const newId = (p: string) => `${p}-c${++n}`;
|
||||
|
||||
it('BKGRP-012: copies of a group become a group of their own, its name carried along', () => {
|
||||
const source = page([shape('a', { group: 'g' }), shape('b', { group: 'g' }), shape('c', { group: 'h' })], {
|
||||
groups: [{ id: 'g', name: 'Lockup' }],
|
||||
});
|
||||
const copies = source.elements.map((e) => ({ ...e, id: `${e.id}2` }));
|
||||
const out = regroupCopies(copies, source, newId);
|
||||
const [a, b, c] = out.elements;
|
||||
expect(a?.group).toBe(b?.group);
|
||||
expect(a?.group).not.toBe('g');
|
||||
// c was the only copy of its group: it goes alone.
|
||||
expect(c && 'group' in c).toBe(false);
|
||||
expect(out.groups).toEqual([{ id: a?.group, name: 'Lockup' }]);
|
||||
const target = withCopies(page([shape('z')]), out);
|
||||
expect(target.elements.map((e) => e.id)).toEqual(['z', 'a2', 'b2', 'c2']);
|
||||
expect(target.groups).toEqual(out.groups);
|
||||
});
|
||||
|
||||
it('BKGRP-013: copies without groups, or without a source to name them from, stay plain', () => {
|
||||
const plain = regroupCopies([shape('a')], undefined, newId);
|
||||
expect(plain.groups).toEqual([]);
|
||||
const unnamed = regroupCopies([shape('a', { group: 'g' }), shape('b', { group: 'g' })], undefined, newId);
|
||||
expect(unnamed.groups).toEqual([]);
|
||||
expect(unnamed.elements[0]?.group).toBe(unnamed.elements[1]?.group);
|
||||
expect(withCopies(page([]), plain).groups).toBeUndefined();
|
||||
expect(withoutGroup(shape('x', { group: 'g' })).group).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
import type { BookElement, BookGroup, BookSpread } from './book.schema';
|
||||
|
||||
/**
|
||||
* Groups on a page, and the layer fields that come with them.
|
||||
*
|
||||
* ── What a group is ──────────────────────────────────────────────────────
|
||||
*
|
||||
* A flat set of elements of one spread that are picked, moved, aligned and
|
||||
* arranged as one. Membership lives on the elements (`group`, an id that is
|
||||
* only meaningful on its own spread); the spread's `groups` list holds what a
|
||||
* group is called. There is no nesting: grouping elements that are already in
|
||||
* groups makes one new group of all of them, which is what a page needs and
|
||||
* what a model can describe without a tree.
|
||||
*
|
||||
* Two rules keep every reader simple:
|
||||
* - a group has at least two members on the page (`tidyGroups` dissolves the
|
||||
* rest, so a lone element never carries a group nobody can see);
|
||||
* - its members are next to each other in the paint order (`gatherGroups`),
|
||||
* so the layers panel can show a group as one row without lying about what
|
||||
* is in front of what.
|
||||
*
|
||||
* The Studio and the book engine (book-ops-groups.ts) both call these, so a
|
||||
* group made by hand and one made by the assistant are the same thing.
|
||||
*/
|
||||
|
||||
/** Every member of a group, in paint order. */
|
||||
export function groupMembers(spread: BookSpread, groupId: string): BookElement[] {
|
||||
return spread.elements.filter((e) => e.group === groupId);
|
||||
}
|
||||
|
||||
/** What a group is called, or undefined when it has no name. */
|
||||
export function bookGroupName(spread: BookSpread, groupId: string): string | undefined {
|
||||
return spread.groups?.find((g) => g.id === groupId)?.name;
|
||||
}
|
||||
|
||||
/** The groups of a spread that have members on the page, in the paint order of their lowest member. */
|
||||
export function spreadGroupIds(spread: BookSpread): string[] {
|
||||
const seen = new Set<string>();
|
||||
for (const el of spread.elements) if (el.group) seen.add(el.group);
|
||||
return [...seen];
|
||||
}
|
||||
|
||||
/**
|
||||
* The ids named plus every other member of their groups, in paint order.
|
||||
*
|
||||
* What a click on a grouped element picks, and what a marquee that touches one
|
||||
* member of a group picks: the group is the thing on the page.
|
||||
*/
|
||||
export function expandToGroups(spread: BookSpread, ids: readonly string[]): string[] {
|
||||
const named = new Set(ids);
|
||||
const groups = new Set<string>();
|
||||
for (const el of spread.elements) if (named.has(el.id) && el.group) groups.add(el.group);
|
||||
return spread.elements
|
||||
.filter((e) => named.has(e.id) || (e.group !== undefined && groups.has(e.group)))
|
||||
.map((e) => e.id);
|
||||
}
|
||||
|
||||
/** The one group every named element is in, or null when they are not all in the same one. */
|
||||
export function commonGroup(spread: BookSpread, ids: readonly string[]): string | null {
|
||||
const els = spread.elements.filter((e) => ids.includes(e.id));
|
||||
const first = els[0]?.group;
|
||||
if (!first || !els.every((e) => e.group === first)) return null;
|
||||
return first;
|
||||
}
|
||||
|
||||
/** Whether the named ids are exactly the members of one group, no more and no fewer. */
|
||||
export function isWholeGroup(spread: BookSpread, ids: readonly string[]): boolean {
|
||||
const group = commonGroup(spread, ids);
|
||||
return group !== null && groupMembers(spread, group).length === new Set(ids).size;
|
||||
}
|
||||
|
||||
/**
|
||||
* The elements moved so every group's members are next to each other, at the
|
||||
* place of the group's topmost member, keeping their order among themselves.
|
||||
* The same spread object when nothing had to move.
|
||||
*/
|
||||
export function gatherGroups(spread: BookSpread): BookSpread {
|
||||
const lastIndex = new Map<string, number>();
|
||||
spread.elements.forEach((e, i) => {
|
||||
if (e.group) lastIndex.set(e.group, i);
|
||||
});
|
||||
if (!lastIndex.size) return spread;
|
||||
const out: BookElement[] = [];
|
||||
spread.elements.forEach((e, i) => {
|
||||
if (!e.group) {
|
||||
out.push(e);
|
||||
return;
|
||||
}
|
||||
if (lastIndex.get(e.group) !== i) return;
|
||||
out.push(...groupMembers(spread, e.group));
|
||||
});
|
||||
return out.every((e, i) => e === spread.elements[i]) ? spread : { ...spread, elements: out };
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups with fewer than two members on the page dissolved, and names of
|
||||
* groups that are gone dropped. The same spread object when all was in order.
|
||||
*/
|
||||
export function tidyGroups(spread: BookSpread): BookSpread {
|
||||
const count = new Map<string, number>();
|
||||
for (const e of spread.elements) if (e.group) count.set(e.group, (count.get(e.group) ?? 0) + 1);
|
||||
const lonely = (e: BookElement) => e.group !== undefined && (count.get(e.group) ?? 0) < 2;
|
||||
const elements = spread.elements.some(lonely)
|
||||
? spread.elements.map((e) => (lonely(e) ? withoutGroup(e) : e))
|
||||
: spread.elements;
|
||||
const live = new Set([...count].filter(([, n]) => n >= 2).map(([id]) => id));
|
||||
const groups = spread.groups?.filter((g) => live.has(g.id));
|
||||
const groupsChanged = spread.groups !== undefined && groups?.length !== spread.groups.length;
|
||||
if (elements === spread.elements && !groupsChanged) return spread;
|
||||
const next: BookSpread = { ...spread, elements };
|
||||
if (groupsChanged) {
|
||||
if (groups?.length) next.groups = groups;
|
||||
else delete next.groups;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** An element without its group. */
|
||||
export function withoutGroup<E extends BookElement>(el: E): E {
|
||||
const next = { ...el };
|
||||
delete next.group;
|
||||
return next;
|
||||
}
|
||||
|
||||
function withName(groups: readonly BookGroup[] | undefined, id: string, name: string | undefined): BookGroup[] {
|
||||
const rest = (groups ?? []).filter((g) => g.id !== id);
|
||||
return name ? [...rest, { id, name }] : rest;
|
||||
}
|
||||
|
||||
function setGroups(spread: BookSpread, groups: BookGroup[]): BookSpread {
|
||||
const next: BookSpread = { ...spread, groups };
|
||||
if (!groups.length) delete next.groups;
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* The named elements made one new group, gathered at the place of the topmost
|
||||
* of them. Elements that were in other groups leave them (and a group left with
|
||||
* one member dissolves). `name` is the group's name, none for an unnamed one.
|
||||
*/
|
||||
export function groupElements(spread: BookSpread, ids: readonly string[], groupId: string, name?: string): BookSpread {
|
||||
const chosen = new Set(ids);
|
||||
const elements = spread.elements.map((e) => (chosen.has(e.id) ? { ...e, group: groupId } : e));
|
||||
const trimmed = name?.trim();
|
||||
return tidyGroups(gatherGroups(setGroups({ ...spread, elements }, withName(spread.groups, groupId, trimmed))));
|
||||
}
|
||||
|
||||
/** A group's members back on their own, where they are in the paint order. */
|
||||
export function ungroupElements(spread: BookSpread, groupId: string): BookSpread {
|
||||
const elements = spread.elements.map((e) => (e.group === groupId ? withoutGroup(e) : e));
|
||||
return tidyGroups({ ...spread, elements });
|
||||
}
|
||||
|
||||
/** A group named, or its name cleared with an empty one. */
|
||||
export function renameGroup(spread: BookSpread, groupId: string, name: string): BookSpread {
|
||||
return setGroups(spread, withName(spread.groups, groupId, name.trim() || undefined));
|
||||
}
|
||||
|
||||
/** An element named, or its name cleared with an empty one. */
|
||||
export function renameElement(spread: BookSpread, id: string, name: string): BookSpread {
|
||||
const trimmed = name.trim();
|
||||
const elements = spread.elements.map((e) => {
|
||||
if (e.id !== id) return e;
|
||||
const next = { ...e };
|
||||
if (trimmed) next.name = trimmed;
|
||||
else delete next.name;
|
||||
return next;
|
||||
});
|
||||
return { ...spread, elements };
|
||||
}
|
||||
|
||||
/** Elements shown or hidden. `hidden: false` drops the field, so a shown element stores what it stored before. */
|
||||
export function setElementsHidden(spread: BookSpread, ids: readonly string[], hidden: boolean): BookSpread {
|
||||
const chosen = new Set(ids);
|
||||
const elements = spread.elements.map((e) => {
|
||||
if (!chosen.has(e.id)) return e;
|
||||
const next = { ...e };
|
||||
if (hidden) next.hidden = true;
|
||||
else delete next.hidden;
|
||||
return next;
|
||||
});
|
||||
return { ...spread, elements };
|
||||
}
|
||||
|
||||
/** The spread as it prints: without its hidden elements. The same object when none is hidden. */
|
||||
export function visibleSpread(spread: BookSpread): BookSpread {
|
||||
if (!spread.elements.some((e) => e.hidden)) return spread;
|
||||
return { ...spread, elements: spread.elements.filter((e) => !e.hidden) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies of elements given groups of their own.
|
||||
*
|
||||
* A copy that kept its source's group id would join the original group on a
|
||||
* duplicate, or another page's group of the same id on a paste. Each group
|
||||
* among the copies gets a fresh id; its name comes along from `source` (the
|
||||
* spread the copies came from), so a copied lockup is still called what it was.
|
||||
* A copy that is the only one of its group among the copies goes alone.
|
||||
*/
|
||||
export function regroupCopies(
|
||||
copies: readonly BookElement[],
|
||||
source: BookSpread | undefined,
|
||||
newId: (prefix: string) => string,
|
||||
): { elements: BookElement[]; groups: BookGroup[] } {
|
||||
const count = new Map<string, number>();
|
||||
for (const e of copies) if (e.group) count.set(e.group, (count.get(e.group) ?? 0) + 1);
|
||||
const fresh = new Map<string, string>();
|
||||
const groups: BookGroup[] = [];
|
||||
for (const [old, n] of count) {
|
||||
if (n < 2) continue;
|
||||
const id = newId('g');
|
||||
fresh.set(old, id);
|
||||
const name = source ? bookGroupName(source, old) : undefined;
|
||||
groups.push(name ? { id, name } : { id });
|
||||
}
|
||||
const elements = copies.map((e) => {
|
||||
if (!e.group) return e;
|
||||
const id = fresh.get(e.group);
|
||||
return id ? { ...e, group: id } : withoutGroup(e);
|
||||
});
|
||||
return { elements, groups: groups.filter((g) => g.name !== undefined) };
|
||||
}
|
||||
|
||||
/** Copies added on top of a spread with the names of their new groups. */
|
||||
export function withCopies(
|
||||
spread: BookSpread,
|
||||
copied: { elements: readonly BookElement[]; groups: readonly BookGroup[] },
|
||||
): BookSpread {
|
||||
const next = { ...spread, elements: [...spread.elements, ...copied.elements] };
|
||||
return copied.groups.length ? { ...next, groups: [...(spread.groups ?? []), ...copied.groups] } : next;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export const BOOK_OP_ERROR_CODES = [
|
||||
'sticker_unknown',
|
||||
'not_enough_elements',
|
||||
'layouts_full',
|
||||
'group_not_found',
|
||||
] as const;
|
||||
export type BookOpErrorCode = (typeof BOOK_OP_ERROR_CODES)[number];
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { relayoutSpread } from './auto/auto-book';
|
||||
import type { AutoEntry, AutoInput } from './auto/auto-types';
|
||||
import { alignElements, arrangeElements, distributableCount, distributeElements } from './book-arrange';
|
||||
import { bookElementPropsSchema, type BOOK_ELEMENT_OP_SCHEMAS, type BookNewElement } from './book-element-ops.schema';
|
||||
import { withRealWeight } from './book-fonts';
|
||||
import { bookGridElements, defaultBookGridBox, findBookGrid } from './book-grids';
|
||||
import { regroupCopies, tidyGroups, withCopies, withoutGroup } from './book-groups';
|
||||
import {
|
||||
bookBoundWords,
|
||||
failOp,
|
||||
@@ -427,94 +429,26 @@ function opLock(spread: BookSpread, op: Op<'lock'>, env: ElementOpEnv): BookSpre
|
||||
);
|
||||
}
|
||||
|
||||
// ── arrange ────────────────────────────────────────────────────────────────
|
||||
|
||||
function opArrange(spread: BookSpread, op: Op<'arrange'>, env: ElementOpEnv): BookSpread {
|
||||
const chosen = new Set(namedElements(spread, op.elementIds, env).map((e) => e.id));
|
||||
const pick = (want: boolean) => spread.elements.filter((e) => chosen.has(e.id) === want);
|
||||
if (op.to === 'front') return { ...spread, elements: [...pick(false), ...pick(true)] };
|
||||
if (op.to === 'back') return { ...spread, elements: [...pick(true), ...pick(false)] };
|
||||
const els = spread.elements.slice();
|
||||
const step = op.to === 'forward' ? 1 : -1;
|
||||
const order = els.map((_, i) => i);
|
||||
if (step === 1) order.reverse();
|
||||
for (const i of order) {
|
||||
const here = els[i];
|
||||
const there = els[i + step];
|
||||
if (!here || !there || !chosen.has(here.id) || chosen.has(there.id)) continue;
|
||||
els[i] = there;
|
||||
els[i + step] = here;
|
||||
}
|
||||
return { ...spread, elements: els };
|
||||
}
|
||||
|
||||
// ── align, distribute ──────────────────────────────────────────────────────
|
||||
// ── arrange, align, distribute (book-arrange.ts) ───────────────────────────
|
||||
|
||||
/** A frame in millimetres, as the book types it, so a zod-inferred `el.frame` passes in every compile mode. */
|
||||
type Box = BookFrame;
|
||||
|
||||
function boundsOf(frames: readonly Box[]): Box {
|
||||
const x = Math.min(...frames.map((f) => f.x));
|
||||
const y = Math.min(...frames.map((f) => f.y));
|
||||
const right = Math.max(...frames.map((f) => f.x + f.w));
|
||||
const bottom = Math.max(...frames.map((f) => f.y + f.h));
|
||||
return { x, y, w: right - x, h: bottom - y };
|
||||
}
|
||||
|
||||
/** The page an element's centre is on, or its safe area. */
|
||||
function pageBox(spread: BookSpread, page: BookPageSetup, f: Box, safe: boolean): Box {
|
||||
const onRight = isInner(spread) && f.x + f.w / 2 >= page.pageWidth;
|
||||
const x = onRight ? page.pageWidth : 0;
|
||||
const inset = safe ? page.safe : 0;
|
||||
return { x: x + inset, y: inset, w: page.pageWidth - inset * 2, h: page.pageHeight - inset * 2 };
|
||||
}
|
||||
|
||||
function alignedFrame(f: Box, box: Box, edge: Op<'align'>['edge']): BookFrame {
|
||||
switch (edge) {
|
||||
case 'left':
|
||||
return { ...f, x: round2(box.x) };
|
||||
case 'center':
|
||||
return { ...f, x: round2(box.x + (box.w - f.w) / 2) };
|
||||
case 'right':
|
||||
return { ...f, x: round2(box.x + box.w - f.w) };
|
||||
case 'top':
|
||||
return { ...f, y: round2(box.y) };
|
||||
case 'middle':
|
||||
return { ...f, y: round2(box.y + (box.h - f.h) / 2) };
|
||||
case 'bottom':
|
||||
return { ...f, y: round2(box.y + box.h - f.h) };
|
||||
}
|
||||
function opArrange(spread: BookSpread, op: Op<'arrange'>, env: ElementOpEnv): BookSpread {
|
||||
const chosen = namedElements(spread, op.elementIds, env).map((e) => e.id);
|
||||
return arrangeElements(spread, chosen, op.to);
|
||||
}
|
||||
|
||||
function opAlign(spread: BookSpread, op: Op<'align'>, env: ElementOpEnv): BookSpread {
|
||||
const els = namedElements(spread, op.elementIds, env);
|
||||
const selection = boundsOf(els.map((e) => e.frame));
|
||||
const next = els.map((e) => {
|
||||
const box = op.to === 'selection' ? selection : pageBox(spread, env.page, e.frame, op.to === 'safe');
|
||||
return { ...e, frame: alignedFrame(e.frame, box, op.edge) } as BookElement;
|
||||
});
|
||||
return replaceAll(spread, next);
|
||||
return alignElements(spread, env.page, namedElements(spread, op.elementIds, env), op.edge, op.to);
|
||||
}
|
||||
|
||||
function opDistribute(spread: BookSpread, op: Op<'distribute'>, env: ElementOpEnv): BookSpread {
|
||||
const els = namedElements(spread, op.elementIds, env);
|
||||
if (els.length < 3) throw failOp('not_enough_elements', 'distribute needs three different elements', env.opIndex);
|
||||
const across = op.axis === 'horizontal';
|
||||
const start = (f: Box) => (across ? f.x : f.y);
|
||||
const size = (f: Box) => (across ? f.w : f.h);
|
||||
const sorted = els.slice().sort((a, b) => start(a.frame) - start(b.frame));
|
||||
const first = sorted[0] as BookElement;
|
||||
const last = sorted[sorted.length - 1] as BookElement;
|
||||
const total = sorted.reduce((sum, e) => sum + size(e.frame), 0);
|
||||
const span = start(last.frame) + size(last.frame) - start(first.frame);
|
||||
const gap = op.gap ?? (span - total) / (sorted.length - 1);
|
||||
let cursor = start(first.frame);
|
||||
const next = sorted.map((e) => {
|
||||
const at = round2(cursor);
|
||||
cursor += size(e.frame) + gap;
|
||||
return { ...e, frame: across ? { ...e.frame, x: at } : { ...e.frame, y: at } } as BookElement;
|
||||
});
|
||||
return replaceAll(spread, next);
|
||||
if (distributableCount(spread, els) < 3) {
|
||||
throw failOp('not_enough_elements', 'distribute needs three different elements or groups', env.opIndex);
|
||||
}
|
||||
return distributeElements(spread, els, op.axis, op.gap);
|
||||
}
|
||||
|
||||
// ── duplicate, fill, copy_from ─────────────────────────────────────────────
|
||||
@@ -530,7 +464,7 @@ function opDuplicate(spread: BookSpread, op: Op<'duplicate'>, env: ElementOpEnv)
|
||||
const frame = { ...e.frame, x: round2(e.frame.x + dx), y: round2(e.frame.y + dy) };
|
||||
return { ...e, id: env.newId(prefixOf(e)), frame } as BookElement;
|
||||
});
|
||||
return { ...spread, elements: [...spread.elements, ...copies] };
|
||||
return withCopies(spread, regroupCopies(copies, spread, env.newId));
|
||||
}
|
||||
|
||||
/** The inspector's fill: the photo to the bleed of the page its centre is on, or of the spread, at the back. */
|
||||
@@ -548,8 +482,9 @@ function opFill(spread: BookSpread, op: Op<'fill'>, env: ElementOpEnv): BookSpre
|
||||
w: (spanning ? 2 : 1) * pageWidth + 2 * bleed,
|
||||
h: pageHeight + 2 * bleed,
|
||||
};
|
||||
const filled: BookElement = { ...el, frame, rotation: 0, fit: 'cover' };
|
||||
return { ...spread, elements: [filled, ...spread.elements.filter((e) => e.id !== el.id)] };
|
||||
// A picture that is the page is nobody's lockup: it leaves its group.
|
||||
const filled: BookElement = { ...withoutGroup(el), frame, rotation: 0, fit: 'cover' };
|
||||
return tidyGroups({ ...spread, elements: [filled, ...spread.elements.filter((e) => e.id !== el.id)] });
|
||||
}
|
||||
|
||||
/** Which page of its spread an element is on: the left (or only) page is 0, the right one starts at the fold. */
|
||||
@@ -568,7 +503,7 @@ function opCopyFrom(spread: BookSpread, op: Op<'copy_from'>, env: ElementOpEnv):
|
||||
const x = sameKind ? e.frame.x : e.frame.x - pageOrigin(source, env.page, e.frame) + target;
|
||||
return { ...e, id: env.newId(prefixOf(e)), frame: { ...e.frame, x: round2(x) } } as BookElement;
|
||||
});
|
||||
return { ...spread, elements: [...spread.elements, ...copies] };
|
||||
return withCopies(spread, regroupCopies(copies, source, env.newId));
|
||||
}
|
||||
|
||||
// ── refresh_travel, add_grid ───────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { spreadFindings } from './book-findings';
|
||||
import { designFindings } from './book-findings-design';
|
||||
import { applyBookOps } from './book-ops';
|
||||
import { isGroupOp } from './book-ops-groups';
|
||||
import { bookPageOpSchema, type BookPageOpInput } from './book-ops.schema';
|
||||
import { blankBook } from './book-pages';
|
||||
import {
|
||||
bookPhotoElementSchema,
|
||||
bookShapeElementSchema,
|
||||
bookSpreadSchema,
|
||||
bookTextElementSchema,
|
||||
type BookDocument,
|
||||
type BookElement,
|
||||
type BookSpread,
|
||||
} from './book.schema';
|
||||
import { describeSpread } from './describe-spread';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* The layer operations the assistant sends, `group`, `ungroup`, `name` and
|
||||
* `hide`, run by the one engine, and what grouping changes about the other
|
||||
* operations, the findings and the words describe_book_page reads out.
|
||||
*/
|
||||
|
||||
let seq = 0;
|
||||
const newId = (p: string) => `${p}-n${++seq}`;
|
||||
|
||||
type Frame = { x: number; y: number; w: number; h: number };
|
||||
|
||||
const text = (id: string, words: string, frame: Frame = { x: 230, y: 120, w: 160, h: 40 }, over = {}) =>
|
||||
bookTextElementSchema.parse({ id, kind: 'text', frame, text: words, size: 10, ...over }) as BookElement;
|
||||
const shape = (id: string, x: number, y: number, over = {}) =>
|
||||
bookShapeElementSchema.parse({
|
||||
id,
|
||||
kind: 'shape',
|
||||
frame: { x, y, w: 20, h: 20 },
|
||||
shape: 'rect',
|
||||
...over,
|
||||
}) as BookElement;
|
||||
const photo = (id: string, frame: Frame, over = {}) =>
|
||||
bookPhotoElementSchema.parse({ id, kind: 'photo', frame, photoId: 1, ...over }) as BookElement;
|
||||
|
||||
function book(elements: BookElement[], over: Partial<BookSpread> = {}): BookDocument {
|
||||
const doc = blankBook({ title: 'Iceland', newId: (p) => `${p}-x` });
|
||||
const ids = ['sp-cover', 'sp-first', 'sp-in', 'sp-last', 'sp-back'];
|
||||
const spreads = doc.spreads.map((s, i) => ({ ...s, id: ids[i] as string }));
|
||||
spreads[2] = { ...(spreads[2] as BookSpread), elements, ...over };
|
||||
return { ...doc, spreads };
|
||||
}
|
||||
|
||||
const inner = (doc: BookDocument) => doc.spreads[2] as BookSpread;
|
||||
const run = (doc: BookDocument, ops: BookPageOpInput[]) => applyBookOps(doc, { spreadId: 'sp-in', ops }, { newId });
|
||||
|
||||
function ok(result: ReturnType<typeof applyBookOps>): BookDocument {
|
||||
if (!result.ok) throw new Error(`refused: ${result.error.code} ${result.error.message}`);
|
||||
expect(bookSpreadSchema.safeParse(inner(result.doc)).success).toBe(true);
|
||||
return result.doc;
|
||||
}
|
||||
const refusal = (result: ReturnType<typeof applyBookOps>) => (result.ok ? null : result.error.code);
|
||||
const order = (doc: BookDocument) => inner(doc).elements.map((e) => `${e.id}${e.group ? ':g' : ''}`);
|
||||
|
||||
describe('the layer operations', () => {
|
||||
const page = () => book([shape('a', 230, 20), text('t', 'Title'), shape('b', 300, 20), shape('c', 330, 20)]);
|
||||
|
||||
it('BKGOPS-001: groups elements under a new id and name, gathered together at the topmost', () => {
|
||||
const doc = ok(run(page(), [{ op: 'group', elementIds: ['a', 'b'], name: 'Sun and sea' }]));
|
||||
expect(order(doc)).toEqual(['t', 'a:g', 'b:g', 'c']);
|
||||
const groupId = inner(doc).elements[1]?.group;
|
||||
expect(groupId).toMatch(/^g-n\d+$/);
|
||||
expect(inner(doc).groups).toEqual([{ id: groupId, name: 'Sun and sea' }]);
|
||||
});
|
||||
|
||||
it('BKGOPS-002: refuses a locked element, a missing one, and one element twice', () => {
|
||||
const locked = book([shape('a', 230, 20, { locked: true }), shape('b', 300, 20)]);
|
||||
expect(refusal(run(locked, [{ op: 'group', elementIds: ['a', 'b'] }]))).toBe('locked');
|
||||
expect(refusal(run(page(), [{ op: 'group', elementIds: ['a', 'zz'] }]))).toBe('element_not_found');
|
||||
expect(refusal(run(page(), [{ op: 'group', elementIds: ['a', 'a'] }]))).toBe('not_enough_elements');
|
||||
});
|
||||
|
||||
it('BKGOPS-003: ungroups, names and hides, locked elements too, and says which group is missing', () => {
|
||||
const grouped = ok(run(page(), [{ op: 'group', elementIds: ['a', 'b'] }]));
|
||||
const groupId = inner(grouped).elements.find((e) => e.id === 'a')?.group as string;
|
||||
const named = ok(
|
||||
run(grouped, [
|
||||
{ op: 'name', groupId, name: 'Pair' },
|
||||
{ op: 'name', elementId: 't', name: 'Heading' },
|
||||
]),
|
||||
);
|
||||
expect(inner(named).groups).toEqual([{ id: groupId, name: 'Pair' }]);
|
||||
expect(inner(named).elements.find((e) => e.id === 't')?.name).toBe('Heading');
|
||||
const apart = ok(run(named, [{ op: 'ungroup', groupId }]));
|
||||
expect(order(apart)).toEqual(['t', 'a', 'b', 'c']);
|
||||
expect(inner(apart).groups).toBeUndefined();
|
||||
expect(refusal(run(apart, [{ op: 'ungroup', groupId }]))).toBe('group_not_found');
|
||||
expect(refusal(run(apart, [{ op: 'name', groupId: 'g-none', name: 'x' }]))).toBe('group_not_found');
|
||||
expect(refusal(run(apart, [{ op: 'name', elementId: 'zz', name: 'x' }]))).toBe('element_not_found');
|
||||
|
||||
const pinned = book([shape('a', 230, 20, { locked: true })]);
|
||||
const hidden = ok(run(pinned, [{ op: 'hide', elementIds: ['a'], hidden: true }]));
|
||||
expect(inner(hidden).elements[0]?.hidden).toBe(true);
|
||||
const shown = ok(run(hidden, [{ op: 'hide', elementIds: ['a'], hidden: false }]));
|
||||
expect('hidden' in (inner(shown).elements[0] as object)).toBe(false);
|
||||
expect(refusal(run(pinned, [{ op: 'hide', elementIds: ['zz'], hidden: true }]))).toBe('element_not_found');
|
||||
});
|
||||
|
||||
it('BKGOPS-004: the contract wants an element or a group to name, and two elements to group', () => {
|
||||
expect(bookPageOpSchema.safeParse({ op: 'name', name: 'x' }).success).toBe(false);
|
||||
expect(bookPageOpSchema.safeParse({ op: 'name', name: 'x', elementId: 'a', groupId: 'g' }).success).toBe(false);
|
||||
expect(bookPageOpSchema.safeParse({ op: 'group', elementIds: ['a'] }).success).toBe(false);
|
||||
expect(bookPageOpSchema.safeParse({ op: 'name', name: 'x'.repeat(61), elementId: 'a' }).success).toBe(false);
|
||||
expect(isGroupOp({ op: 'group' })).toBe(true);
|
||||
expect(isGroupOp({ op: 'align' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the other operations, with groups', () => {
|
||||
const grouped = () =>
|
||||
book([shape('a', 230, 20, { group: 'g' }), shape('b', 260, 40, { group: 'g' }), shape('c', 330, 20)], {
|
||||
groups: [{ id: 'g', name: 'Lockup' }],
|
||||
});
|
||||
|
||||
it('BKGOPS-005: a duplicate of a group is a new group of the same name, never part of the old one', () => {
|
||||
const doc = ok(run(grouped(), [{ op: 'duplicate', elementIds: ['a', 'b'] }]));
|
||||
const els = inner(doc).elements;
|
||||
const copies = els.slice(3);
|
||||
expect(copies[0]?.group).toBe(copies[1]?.group);
|
||||
expect(copies[0]?.group).not.toBe('g');
|
||||
expect(inner(doc).groups).toEqual([
|
||||
{ id: 'g', name: 'Lockup' },
|
||||
{ id: copies[0]?.group, name: 'Lockup' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('BKGOPS-006: copy_from brings a group as a group of its own', () => {
|
||||
const doc = grouped();
|
||||
const withSource = {
|
||||
...doc,
|
||||
spreads: doc.spreads.map((s, i) => (i === 3 ? { ...inner(doc), id: 'sp-last', role: 'last' as const } : s)),
|
||||
};
|
||||
const empty = {
|
||||
...withSource,
|
||||
spreads: withSource.spreads.map((s, i) => (i === 2 ? { ...s, elements: [], groups: undefined } : s)),
|
||||
};
|
||||
const done = ok(
|
||||
applyBookOps(
|
||||
empty,
|
||||
{ spreadId: 'sp-in', ops: [{ op: 'copy_from', spreadId: 'sp-last', elementIds: ['a', 'b'] }] },
|
||||
{ newId },
|
||||
),
|
||||
);
|
||||
const [a, b] = inner(done).elements;
|
||||
expect(a?.group).toBe(b?.group);
|
||||
expect(a?.group).not.toBe('g');
|
||||
expect(inner(done).groups?.[0]?.name).toBe('Lockup');
|
||||
});
|
||||
|
||||
it('BKGOPS-007: removing a member leaves no group of one; aligning moves the group as one box', () => {
|
||||
const removed = ok(run(grouped(), [{ op: 'remove', elementIds: ['a'], park: false }]));
|
||||
expect(order(removed)).toEqual(['b', 'c']);
|
||||
expect(inner(removed).groups).toBeUndefined();
|
||||
const aligned = ok(run(grouped(), [{ op: 'align', elementIds: ['a', 'b', 'c'], edge: 'top' }]));
|
||||
const ys = inner(aligned).elements.map((e) => e.frame.y);
|
||||
// The group's box starts at 20 already, so it stays; c is at 20 too.
|
||||
expect(ys).toEqual([20, 40, 20]);
|
||||
const arranged = ok(run(grouped(), [{ op: 'arrange', elementIds: ['c'], to: 'back' }]));
|
||||
expect(order(arranged)).toEqual(['c', 'a:g', 'b:g']);
|
||||
});
|
||||
|
||||
it('BKGOPS-008: distribute counts a group once', () => {
|
||||
const doc = book([
|
||||
shape('a', 0, 0),
|
||||
shape('g1', 50, 0, { group: 'g' }),
|
||||
shape('g2', 60, 0, { group: 'g' }),
|
||||
shape('c', 200, 0),
|
||||
]);
|
||||
const spaced = ok(run(doc, [{ op: 'distribute', elementIds: ['a', 'g1', 'g2', 'c'], axis: 'horizontal' }]));
|
||||
const x = (id: string) => inner(spaced).elements.find((e) => e.id === id)?.frame.x;
|
||||
// Span 0..220, sizes 20 + 30 + 20, so gaps of 75; the group keeps its shape.
|
||||
expect([x('g1'), x('g2'), x('c')]).toEqual([95, 105, 200]);
|
||||
const two = book([shape('g1', 50, 0, { group: 'g' }), shape('g2', 60, 0, { group: 'g' }), shape('c', 200, 0)]);
|
||||
expect(refusal(run(two, [{ op: 'distribute', elementIds: ['g1', 'g2', 'c'], axis: 'horizontal' }]))).toBe(
|
||||
'not_enough_elements',
|
||||
);
|
||||
});
|
||||
|
||||
it('BKGOPS-009: a picture filled to the page leaves its group', () => {
|
||||
const doc = book([
|
||||
photo('p', { x: 230, y: 20, w: 50, h: 40 }, { group: 'g' }),
|
||||
shape('s', 300, 20, { group: 'g' }),
|
||||
]);
|
||||
const filled = ok(run(doc, [{ op: 'fill', elementId: 'p', scope: 'page' }]));
|
||||
expect(inner(filled).elements.every((e) => e.group === undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('what a model reads of groups', () => {
|
||||
it('BKGOPS-010: describe_book_page names the groups, their members, the hidden and the named', () => {
|
||||
const doc = book(
|
||||
[
|
||||
text('t1', 'Iceland', undefined, { group: 'g-1', name: 'Title' }),
|
||||
text('t2', 'June 2026', { x: 230, y: 170, w: 160, h: 20 }, { group: 'g-1' }),
|
||||
shape('s', 300, 20, { hidden: true }),
|
||||
],
|
||||
{ groups: [{ id: 'g-1', name: 'Title lockup' }] },
|
||||
);
|
||||
const page = describeSpread(doc, 'sp-in');
|
||||
expect(page?.text).toContain(
|
||||
'Groups, each picked, moved, aligned and arranged as one: g-1 "Title lockup" (t1, t2)',
|
||||
);
|
||||
expect(page?.text).toContain('in group g-1');
|
||||
expect(page?.text).toContain('named "Title"');
|
||||
expect(page?.text).toContain('hidden (not on the page or in print)');
|
||||
expect(page?.elements[0]).toMatchObject({ group: 'g-1', name: 'Title' });
|
||||
expect(page?.elements[2]).toMatchObject({ hidden: true });
|
||||
expect(describeSpread(book([shape('s', 300, 20)]), 'sp-in')?.text).not.toContain('Groups,');
|
||||
});
|
||||
|
||||
it('BKGOPS-011: two texts set over each other inside one group are a lockup, not an accident', () => {
|
||||
const over = [
|
||||
text('t1', 'Iceland', { x: 230, y: 120, w: 160, h: 40 }),
|
||||
text('t2', 'Island', { x: 235, y: 125, w: 160, h: 40 }),
|
||||
];
|
||||
const loose = spreadFindings(inner(book(over)), book(over).page);
|
||||
expect(loose.some((f) => f.code === 'overlap')).toBe(true);
|
||||
const lockup = over.map((e) => ({ ...e, group: 'g' }) as BookElement);
|
||||
expect(spreadFindings(inner(book(lockup)), book(lockup).page).some((f) => f.code === 'overlap')).toBe(false);
|
||||
});
|
||||
|
||||
it('BKGOPS-012: a hidden element is not on the page, so the checks leave it out', () => {
|
||||
const els = [
|
||||
photo('p1', { x: 230, y: 20, w: 80, h: 60 }),
|
||||
photo('p2', { x: 240, y: 30, w: 80, h: 60 }, { hidden: true }),
|
||||
];
|
||||
const doc = book(els);
|
||||
expect(spreadFindings(inner(doc), doc.page).some((f) => f.elementIds?.includes('p2'))).toBe(false);
|
||||
expect(designFindings(inner(doc), doc.page).some((f) => f.elementIds?.includes('p2'))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { BOOK_GROUP_OP_SCHEMAS } from './book-group-ops.schema';
|
||||
import {
|
||||
groupElements,
|
||||
groupMembers,
|
||||
renameElement,
|
||||
renameGroup,
|
||||
setElementsHidden,
|
||||
ungroupElements,
|
||||
} from './book-groups';
|
||||
import { failOp, pageElement, unlockedElement } from './book-ops-core';
|
||||
import type { BookSpread } from './book.schema';
|
||||
|
||||
import type { z } from 'zod';
|
||||
|
||||
/**
|
||||
* What `group`, `ungroup`, `name` and `hide` do to a page, for `applyBookOps`.
|
||||
*
|
||||
* Grouping moves the members next to each other in the paint order, so a
|
||||
* locked element refuses it like any other change. Ungrouping, naming and
|
||||
* hiding move nothing on the page and take locked elements too, as the
|
||||
* Studio's layers panel does: a locked background can still be hidden to get
|
||||
* at what is behind it.
|
||||
*/
|
||||
|
||||
type GroupOp = z.infer<(typeof BOOK_GROUP_OP_SCHEMAS)[number]>;
|
||||
|
||||
interface GroupOpEnv {
|
||||
newId: (prefix: string) => string;
|
||||
opIndex: number;
|
||||
}
|
||||
|
||||
const BOOK_GROUP_OP_NAMES: ReadonlySet<string> = new Set(['group', 'ungroup', 'name', 'hide']);
|
||||
|
||||
export function isGroupOp(op: { op: string }): op is GroupOp {
|
||||
return BOOK_GROUP_OP_NAMES.has(op.op);
|
||||
}
|
||||
|
||||
function existingGroup(spread: BookSpread, groupId: string, opIndex: number): void {
|
||||
if (!groupMembers(spread, groupId).length) {
|
||||
throw failOp('group_not_found', `no group ${groupId} on this page`, opIndex);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyGroupOp(spread: BookSpread, op: GroupOp, env: GroupOpEnv): BookSpread {
|
||||
switch (op.op) {
|
||||
case 'group': {
|
||||
const ids = [...new Set(op.elementIds)];
|
||||
for (const id of ids) unlockedElement(pageElement(spread, id, env.opIndex), env.opIndex);
|
||||
if (ids.length < 2) throw failOp('not_enough_elements', 'a group needs two different elements', env.opIndex);
|
||||
return groupElements(spread, ids, env.newId('g'), op.name);
|
||||
}
|
||||
case 'ungroup':
|
||||
existingGroup(spread, op.groupId, env.opIndex);
|
||||
return ungroupElements(spread, op.groupId);
|
||||
case 'name':
|
||||
if (op.groupId !== undefined) {
|
||||
existingGroup(spread, op.groupId, env.opIndex);
|
||||
return renameGroup(spread, op.groupId, op.name);
|
||||
}
|
||||
pageElement(spread, op.elementId ?? '', env.opIndex);
|
||||
return renameElement(spread, op.elementId ?? '', op.name);
|
||||
case 'hide': {
|
||||
const ids = [...new Set(op.elementIds)];
|
||||
for (const id of ids) pageElement(spread, id, env.opIndex);
|
||||
return setElementsHidden(spread, ids, op.hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { idSchema } from '../common/primitives.schema';
|
||||
import { BOOK_ELEMENT_OP_SCHEMAS, TEXT_STYLE_FIELDS, bindingNamesItsSource } from './book-element-ops.schema';
|
||||
import { BOOK_GROUP_OP_SCHEMAS } from './book-group-ops.schema';
|
||||
import { BOOK_COVER_TEMPLATES, BOOK_TEMPLATES } from './book-layouts';
|
||||
import {
|
||||
BOOK_BINDING_HINT,
|
||||
@@ -198,6 +199,7 @@ export const bookPageOpSchema = z.discriminatedUnion('op', [
|
||||
})
|
||||
.refine((v) => (v.color === undefined) !== (v.paint === undefined), { message: 'give the color or the paint' }),
|
||||
...BOOK_ELEMENT_OP_SCHEMAS,
|
||||
...BOOK_GROUP_OP_SCHEMAS,
|
||||
]);
|
||||
export type BookPageOp = z.infer<typeof bookPageOpSchema>;
|
||||
export type BookPageOpInput = z.input<typeof bookPageOpSchema>;
|
||||
@@ -459,6 +461,10 @@ export const bookElementSummarySchema = z.object({
|
||||
photoId: z.number().int().nullable().optional(),
|
||||
text: z.string().max(80).optional(),
|
||||
binding: z.string().optional(),
|
||||
/** The group it is in, its layer name, and whether it is hidden; absent when not. */
|
||||
group: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
});
|
||||
export type BookElementSummary = z.infer<typeof bookElementSummarySchema>;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withRealWeight } from './book-fonts';
|
||||
import { bookGridElements, defaultBookGridBox, findBookGrid, type BookGridDef } from './book-grids';
|
||||
import { tidyGroups } from './book-groups';
|
||||
import { bookElementId } from './book-ids';
|
||||
import { BOOK_TEXT_STYLES, applyBookTemplate, classifyBookTexts, findBookTemplate } from './book-layouts';
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
type BookPhotoInfo,
|
||||
} from './book-ops-core';
|
||||
import { applyElementOp, autoRelayout } from './book-ops-elements';
|
||||
import { applyGroupOp, isGroupOp } from './book-ops-groups';
|
||||
import { BOOK_OP_LIMITS, bookPageOpSchema, type BookPageOp } from './book-ops.schema';
|
||||
import { withSpreadPaint } from './book-paint';
|
||||
import { applySavedBookLayout, bookLayoutRole } from './book-saved-layouts';
|
||||
@@ -88,6 +90,8 @@ export function applyBookOps(
|
||||
env.opIndex = i;
|
||||
spread = applyOne(spread, op, env);
|
||||
});
|
||||
// A removal or a layout can leave a group with one member: it goes, with its name.
|
||||
spread = tidyGroups(spread);
|
||||
checkedSpread(spread, null);
|
||||
const spreads = doc.spreads.slice();
|
||||
spreads[index] = spread;
|
||||
@@ -125,7 +129,7 @@ function applyOne(spread: BookSpread, op: BookPageOp, env: Env): BookSpread {
|
||||
op.paint !== undefined ? op.paint : op.color ? { type: 'solid', color: op.color } : null,
|
||||
);
|
||||
default:
|
||||
return applyElementOp(spread, op, env);
|
||||
return isGroupOp(op) ? applyGroupOp(spread, op, env) : applyElementOp(spread, op, env);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,6 @@ import { describe, expect, it } from 'vitest';
|
||||
*/
|
||||
|
||||
const frame = { x: 12, y: 18, w: 60, h: 40 };
|
||||
/** A revision newer than this build reads. */
|
||||
const NEWER = BOOK_SCHEMA_REV + 1;
|
||||
|
||||
/** A revision one past what this build reads. */
|
||||
const NEWER = BOOK_SCHEMA_REV + 1;
|
||||
|
||||
/** A revision above this build's, and an element kind no revision of it knows. */
|
||||
const NEWER = BOOK_SCHEMA_REV + 1;
|
||||
const fromLater = { id: 'x1', kind: 'hologram', frame, glow: 'lemon' };
|
||||
|
||||
@@ -34,8 +34,9 @@ import type { BookDocument } from './book.schema';
|
||||
* - 2: the wider type, new families and weights (book-fonts.data.ts).
|
||||
* - 3: paint on shapes and pages (`paint`, `backgroundPaint`, book-paint.schema.ts).
|
||||
* - 4: the sticker kind (book-sticker.ts, stickers/).
|
||||
* - 5: groups, names and hidden layers (`group`, `name`, `hidden`, a spread's `groups`).
|
||||
*/
|
||||
export const BOOK_SCHEMA_REV = 4;
|
||||
export const BOOK_SCHEMA_REV = 5;
|
||||
|
||||
/** The `code` a refused save carries in its body when the stored book is newer than the writer. */
|
||||
export const BOOK_SCHEMA_NEWER_CODE = 'book_schema_newer';
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { bookElementStyle, bookStyleKeys, pastableStyle } from './book-style-copy';
|
||||
import {
|
||||
bookIconElementSchema,
|
||||
bookListElementSchema,
|
||||
bookShapeElementSchema,
|
||||
bookTextElementSchema,
|
||||
type BookElement,
|
||||
} from './book.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/** Copy and paste style: the look `style` may set, carried onto any kind that takes it. */
|
||||
|
||||
const frame = { x: 0, y: 0, w: 40, h: 20 };
|
||||
const text = (over = {}) =>
|
||||
bookTextElementSchema.parse({ id: 't', kind: 'text', frame, text: 'Hi', ...over }) as BookElement;
|
||||
const icon = (over = {}) =>
|
||||
bookIconElementSchema.parse({ id: 'i', kind: 'icon', frame, name: 'Sun', ...over }) as BookElement;
|
||||
const shape = (over = {}) =>
|
||||
bookShapeElementSchema.parse({ id: 's', kind: 'shape', frame, shape: 'rect', ...over }) as BookElement;
|
||||
|
||||
describe('copy style', () => {
|
||||
it('BKSTY-001: takes the look of an element and none of what it says or where it is', () => {
|
||||
const style = bookElementStyle(text({ size: 24, color: '#ff0000', weight: 700, opacity: 0.5 }));
|
||||
expect(style.kind).toBe('text');
|
||||
expect(style.props).toMatchObject({ size: 24, color: '#ff0000', weight: 700, opacity: 0.5 });
|
||||
expect(style.props).not.toHaveProperty('text');
|
||||
expect(style.props).not.toHaveProperty('frame');
|
||||
});
|
||||
|
||||
it('BKSTY-002: leaves out the props of `style` that are words rather than looks', () => {
|
||||
const list = bookListElementSchema.parse({ id: 'l', kind: 'list', frame, proLabel: 'Good' }) as BookElement;
|
||||
expect(bookStyleKeys('list')).not.toContain('proLabel');
|
||||
expect(bookElementStyle(list).props).not.toHaveProperty('proLabel');
|
||||
expect(bookStyleKeys('stats')).not.toContain('metrics');
|
||||
});
|
||||
|
||||
it('BKSTY-003: pastes all of it onto the same kind, and what both have onto another', () => {
|
||||
const style = bookElementStyle(text({ size: 24, color: '#ff0000', opacity: 0.5 }));
|
||||
expect(pastableStyle(style, 'text')).toMatchObject({ kind: 'text', size: 24, color: '#ff0000' });
|
||||
// An icon has a colour and an opacity, no type size.
|
||||
const onIcon = pastableStyle(style, 'icon');
|
||||
expect(onIcon).toEqual({ kind: 'icon', color: '#ff0000', opacity: 0.5 });
|
||||
});
|
||||
|
||||
it('BKSTY-004: answers null when the two kinds share nothing that was set', () => {
|
||||
const style = { kind: 'icon' as const, props: { lineWidth: 2 } };
|
||||
expect(pastableStyle(style, 'shape')).toBeNull();
|
||||
expect(pastableStyle(bookElementStyle(shape({ fill: '#00ff00' })), 'shape')).toMatchObject({ fill: '#00ff00' });
|
||||
expect(pastableStyle(bookElementStyle(icon({ color: '#123456' })), 'icon')).toMatchObject({ color: '#123456' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { bookElementPropsSchema, type BookElementProps } from './book-element-ops.schema';
|
||||
import type { BookElement } from './book.schema';
|
||||
|
||||
/**
|
||||
* Copy and paste style: the look of one element taken onto others.
|
||||
*
|
||||
* The look is exactly what the `style` operation may set for the element's
|
||||
* kind (book-element-ops.schema.ts), less the few props that are words rather
|
||||
* than looks (a list's column labels, the figures a stats block shows). So a
|
||||
* pasted style is one `style` operation per element, the same thing the
|
||||
* assistant sends, and onto an element of another kind it carries what both
|
||||
* kinds have: a text's colour onto an icon, the opacity onto anything.
|
||||
*/
|
||||
|
||||
export interface BookCopiedStyle {
|
||||
kind: BookElement['kind'];
|
||||
props: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Props of `style` that say what an element shows, not how it looks. */
|
||||
const CONTENT_KEYS: ReadonlySet<string> = new Set(['metrics', 'proLabel', 'conLabel']);
|
||||
|
||||
const STYLE_KEYS = new Map<string, string[]>(
|
||||
bookElementPropsSchema.options.map((o) => [
|
||||
o.shape.kind.value,
|
||||
Object.keys(o.shape).filter((k) => k !== 'kind' && !CONTENT_KEYS.has(k)),
|
||||
]),
|
||||
);
|
||||
|
||||
/** The look fields an element of a kind takes from a pasted style. */
|
||||
export function bookStyleKeys(kind: BookElement['kind']): readonly string[] {
|
||||
return STYLE_KEYS.get(kind) ?? [];
|
||||
}
|
||||
|
||||
/** The look of an element, as the clipboard keeps it. */
|
||||
export function bookElementStyle(el: BookElement): BookCopiedStyle {
|
||||
const source = el as unknown as Record<string, unknown>;
|
||||
const props: Record<string, unknown> = {};
|
||||
for (const key of bookStyleKeys(el.kind)) if (source[key] !== undefined) props[key] = source[key];
|
||||
return { kind: el.kind, props };
|
||||
}
|
||||
|
||||
/**
|
||||
* The `style` operation's `set` for pasting a copied look onto an element of
|
||||
* `kind`: what both kinds have, or null when they share nothing.
|
||||
*/
|
||||
export function pastableStyle(style: BookCopiedStyle, kind: BookElement['kind']): BookElementProps | null {
|
||||
const set: Record<string, unknown> = { kind };
|
||||
let any = false;
|
||||
for (const key of bookStyleKeys(kind)) {
|
||||
if (style.props[key] === undefined) continue;
|
||||
set[key] = style.props[key];
|
||||
any = true;
|
||||
}
|
||||
if (!any) return null;
|
||||
const parsed = bookElementPropsSchema.safeParse(set);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
@@ -171,6 +171,10 @@ export type BookShapeId = (typeof BOOK_SHAPES)[number];
|
||||
export { BOOK_FONTS_IDS, type BookFontFamily };
|
||||
const bookWeightSchema = z.literal(BOOK_WEIGHTS);
|
||||
|
||||
/** How long a group id and a layer's name may be. */
|
||||
export const BOOK_GROUP_ID_MAX = 40;
|
||||
export const BOOK_LAYER_NAME_MAX = 60;
|
||||
|
||||
export const bookFrameSchema = z.object({
|
||||
/** Millimetres from the left edge of the spread. Negative means bleed. */
|
||||
x: mm,
|
||||
@@ -188,6 +192,16 @@ const elementBase = {
|
||||
rotation: z.number().finite().default(0),
|
||||
opacity: z.number().min(0).max(1).default(1),
|
||||
locked: z.boolean().default(false),
|
||||
/**
|
||||
* The group the element belongs to on its spread, by the group's id (see
|
||||
* book-groups.ts). Absent for an element on its own. Optional rather than
|
||||
* defaulted, so a book without groups stores what it stored before.
|
||||
*/
|
||||
group: z.string().min(1).max(BOOK_GROUP_ID_MAX).optional(),
|
||||
/** What the layers panel calls it. Absent means the name of its kind. */
|
||||
name: z.string().max(BOOK_LAYER_NAME_MAX).optional(),
|
||||
/** Kept in the layers panel but left off the page, on screen and in print. */
|
||||
hidden: z.boolean().optional(),
|
||||
};
|
||||
|
||||
export const bookPhotoElementSchema = z.object({
|
||||
@@ -840,6 +854,13 @@ export type BookBadgeElement = z.infer<typeof bookBadgeElementSchema>;
|
||||
export type BookListElement = z.infer<typeof bookListElementSchema>;
|
||||
export type BookUnits = z.infer<typeof bookUnitsSchema>;
|
||||
|
||||
/** A named group of a spread. */
|
||||
export const bookGroupSchema = z.object({
|
||||
id: z.string().min(1).max(BOOK_GROUP_ID_MAX),
|
||||
name: z.string().max(BOOK_LAYER_NAME_MAX).optional(),
|
||||
});
|
||||
export type BookGroup = z.infer<typeof bookGroupSchema>;
|
||||
|
||||
export const bookSpreadSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
/**
|
||||
@@ -870,6 +891,12 @@ export const bookSpreadSchema = z.object({
|
||||
parked: z.array(bookElementSchema).max(MAX_SPREAD_ELEMENTS).default([]),
|
||||
/** The journey entry this spread was generated from, if any. */
|
||||
entryId: z.number().int().nullable().default(null),
|
||||
/**
|
||||
* The names of the spread's groups. Membership is on the elements (`group`);
|
||||
* a group without a row here is simply unnamed. A list that does not read is
|
||||
* dropped rather than the page.
|
||||
*/
|
||||
groups: z.array(bookGroupSchema).max(MAX_SPREAD_ELEMENTS).optional().catch(undefined),
|
||||
});
|
||||
export type BookSpread = z.infer<typeof bookSpreadSchema>;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { photoDpi, spreadFindings } from './book-findings';
|
||||
import { designFindings, sortBookFindings } from './book-findings-design';
|
||||
import { bookFontRole, bookWeightWord, nearestBookWeight } from './book-fonts';
|
||||
import { BOOK_GRIDS } from './book-grids';
|
||||
import { bookGroupName, groupMembers, spreadGroupIds } from './book-groups';
|
||||
import { BOOK_COVER_TEMPLATES, BOOK_TEMPLATES, type BookTemplate } from './book-layouts';
|
||||
import type { BookPhotoInfo } from './book-ops-core';
|
||||
import type {
|
||||
@@ -150,11 +151,26 @@ function elementLine(el: BookElement, i: number, photos: readonly BookPhotoInfo[
|
||||
el.rotation ? `turned ${n(el.rotation)} deg` : '',
|
||||
el.opacity < 1 ? `opacity ${n(el.opacity)}` : '',
|
||||
el.locked ? 'locked' : '',
|
||||
el.hidden ? 'hidden (not on the page or in print)' : '',
|
||||
el.group ? `in group ${el.group}` : '',
|
||||
el.name ? `named "${head(el.name, 40)}"` : '',
|
||||
].filter(Boolean);
|
||||
const tail = extra.length ? `; ${extra.join(', ')}` : '';
|
||||
return `${i + 1}. ${el.kind} ${el.id} at ${n(x)}, ${n(y)}, ${n(w)} x ${n(h)}: ${elementWords(el, photos)}${tail}`;
|
||||
}
|
||||
|
||||
/** The page's groups by id, with their names and members, so a model can move or align one as a whole. */
|
||||
function groupsLine(spread: BookSpread): string | null {
|
||||
const ids = spreadGroupIds(spread);
|
||||
if (!ids.length) return null;
|
||||
const rows = ids.map((id) => {
|
||||
const name = bookGroupName(spread, id);
|
||||
const members = groupMembers(spread, id).map((e) => e.id);
|
||||
return `${id}${name ? ` "${head(name, 40)}"` : ''} (${members.join(', ')})`;
|
||||
});
|
||||
return `Groups, each picked, moved, aligned and arranged as one: ${rows.join('; ')}`;
|
||||
}
|
||||
|
||||
function parkedLine(spread: BookSpread): string | null {
|
||||
if (!spread.parked.length) return null;
|
||||
const items = spread.parked
|
||||
@@ -200,6 +216,9 @@ function elementSummary(el: BookElement): BookElementSummary {
|
||||
...(el.kind === 'photo' ? { photoId: el.photoId } : {}),
|
||||
...(el.kind === 'text' ? { text: head(el.text, 80) } : {}),
|
||||
...(el.kind === 'text' && el.binding ? { binding: el.binding.source } : {}),
|
||||
...(el.group ? { group: el.group } : {}),
|
||||
...(el.name ? { name: el.name } : {}),
|
||||
...(el.hidden ? { hidden: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -238,6 +257,8 @@ export function describeSpread(
|
||||
} else {
|
||||
lines.push('The page is empty.');
|
||||
}
|
||||
const groups = groupsLine(spread);
|
||||
if (groups) lines.push(groups);
|
||||
const parked = parkedLine(spread);
|
||||
if (parked) lines.push(parked);
|
||||
lines.push(findings.length ? 'Findings:' : 'Findings: none.');
|
||||
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'الطبقات',
|
||||
'journey.studio.layers.empty': 'لا شيء في هذه الصفحة بعد. ما تضيفه يظهر هنا، من الأمام إلى الخلف.',
|
||||
'journey.studio.layers.hint':
|
||||
'من الأمام إلى الخلف. اسحب صفًا لإعادة ترتيبه، وانقر نقرًا مزدوجًا على اسم لإعادة تسميته.',
|
||||
'journey.studio.layers.group': 'مجموعة',
|
||||
'journey.studio.layers.groupAction': 'تجميع',
|
||||
'journey.studio.layers.ungroup': 'فك التجميع',
|
||||
'journey.studio.layers.hide': 'إخفاء',
|
||||
'journey.studio.layers.show': 'إظهار',
|
||||
'journey.studio.layers.rename': 'إعادة تسمية',
|
||||
'journey.studio.layers.expand': 'إظهار المحتوى',
|
||||
'journey.studio.layers.collapse': 'طي',
|
||||
'journey.studio.layers.align': 'محاذاة',
|
||||
'journey.studio.layers.alignLeft': 'محاذاة الحواف اليسرى',
|
||||
'journey.studio.layers.alignCenter': 'توسيط أفقي',
|
||||
'journey.studio.layers.alignRight': 'محاذاة الحواف اليمنى',
|
||||
'journey.studio.layers.alignTop': 'محاذاة الحواف العلوية',
|
||||
'journey.studio.layers.alignMiddle': 'توسيط عمودي',
|
||||
'journey.studio.layers.alignBottom': 'محاذاة الحواف السفلية',
|
||||
'journey.studio.layers.alignTo': 'بالنسبة إلى',
|
||||
'journey.studio.layers.toSelection': 'التحديد',
|
||||
'journey.studio.layers.toPage': 'الصفحة',
|
||||
'journey.studio.layers.toSafe': 'المنطقة الآمنة',
|
||||
'journey.studio.layers.distributeH': 'توزيع متساوٍ أفقيًا',
|
||||
'journey.studio.layers.distributeV': 'توزيع متساوٍ عموديًا',
|
||||
'journey.studio.layers.copyStyle': 'نسخ النمط',
|
||||
'journey.studio.layers.pasteStyle': 'لصق النمط',
|
||||
'journey.studio.layers.groupHint': 'انقر نقرًا مزدوجًا على عنصر للعمل داخل المجموعة.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Laylar',
|
||||
'journey.studio.layers.empty': 'Bu səhifədə hələ heç nə yoxdur. Əlavə etdiyin hər şey burada, öndən arxaya görünür.',
|
||||
'journey.studio.layers.hint':
|
||||
'Öndən arxaya. Sıranı dəyişmək üçün sətri sürüşdür, adı dəyişmək üçün ada iki dəfə klik et.',
|
||||
'journey.studio.layers.group': 'Qrup',
|
||||
'journey.studio.layers.groupAction': 'Qruplaşdır',
|
||||
'journey.studio.layers.ungroup': 'Qrupu ləğv et',
|
||||
'journey.studio.layers.hide': 'Gizlət',
|
||||
'journey.studio.layers.show': 'Göstər',
|
||||
'journey.studio.layers.rename': 'Adını dəyiş',
|
||||
'journey.studio.layers.expand': 'Məzmunu göstər',
|
||||
'journey.studio.layers.collapse': 'Yığ',
|
||||
'journey.studio.layers.align': 'Düzləndir',
|
||||
'journey.studio.layers.alignLeft': 'Sol kənarları düzləndir',
|
||||
'journey.studio.layers.alignCenter': 'Üfüqi mərkəzləşdir',
|
||||
'journey.studio.layers.alignRight': 'Sağ kənarları düzləndir',
|
||||
'journey.studio.layers.alignTop': 'Yuxarı kənarları düzləndir',
|
||||
'journey.studio.layers.alignMiddle': 'Şaquli mərkəzləşdir',
|
||||
'journey.studio.layers.alignBottom': 'Aşağı kənarları düzləndir',
|
||||
'journey.studio.layers.alignTo': 'Nəyə görə',
|
||||
'journey.studio.layers.toSelection': 'Seçim',
|
||||
'journey.studio.layers.toPage': 'Səhifə',
|
||||
'journey.studio.layers.toSafe': 'Təhlükəsiz sahə',
|
||||
'journey.studio.layers.distributeH': 'Üfüqi bərabər payla',
|
||||
'journey.studio.layers.distributeV': 'Şaquli bərabər payla',
|
||||
'journey.studio.layers.copyStyle': 'Üslubu kopyala',
|
||||
'journey.studio.layers.pasteStyle': 'Üslubu yapışdır',
|
||||
'journey.studio.layers.groupHint': 'Qrupun içində işləmək üçün elementə iki dəfə klik et.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Camadas',
|
||||
'journey.studio.layers.empty':
|
||||
'Ainda não há nada nesta página. O que você adicionar aparece aqui, da frente para trás.',
|
||||
'journey.studio.layers.hint':
|
||||
'Da frente para trás. Arraste uma linha para reordenar, clique duas vezes num nome para renomear.',
|
||||
'journey.studio.layers.group': 'Grupo',
|
||||
'journey.studio.layers.groupAction': 'Agrupar',
|
||||
'journey.studio.layers.ungroup': 'Desagrupar',
|
||||
'journey.studio.layers.hide': 'Ocultar',
|
||||
'journey.studio.layers.show': 'Mostrar',
|
||||
'journey.studio.layers.rename': 'Renomear',
|
||||
'journey.studio.layers.expand': 'Mostrar conteúdo',
|
||||
'journey.studio.layers.collapse': 'Recolher',
|
||||
'journey.studio.layers.align': 'Alinhar',
|
||||
'journey.studio.layers.alignLeft': 'Alinhar bordas esquerdas',
|
||||
'journey.studio.layers.alignCenter': 'Centralizar na horizontal',
|
||||
'journey.studio.layers.alignRight': 'Alinhar bordas direitas',
|
||||
'journey.studio.layers.alignTop': 'Alinhar bordas superiores',
|
||||
'journey.studio.layers.alignMiddle': 'Centralizar na vertical',
|
||||
'journey.studio.layers.alignBottom': 'Alinhar bordas inferiores',
|
||||
'journey.studio.layers.alignTo': 'Em relação a',
|
||||
'journey.studio.layers.toSelection': 'Seleção',
|
||||
'journey.studio.layers.toPage': 'Página',
|
||||
'journey.studio.layers.toSafe': 'Área de segurança',
|
||||
'journey.studio.layers.distributeH': 'Distribuir na horizontal',
|
||||
'journey.studio.layers.distributeV': 'Distribuir na vertical',
|
||||
'journey.studio.layers.copyStyle': 'Copiar estilo',
|
||||
'journey.studio.layers.pasteStyle': 'Colar estilo',
|
||||
'journey.studio.layers.groupHint': 'Clique duas vezes num elemento para trabalhar dentro do grupo.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Capes',
|
||||
'journey.studio.layers.empty':
|
||||
'Encara no hi ha res en aquesta pàgina. El que hi afegeixis apareix aquí, de davant cap enrere.',
|
||||
'journey.studio.layers.hint':
|
||||
'De davant cap enrere. Arrossega una fila per reordenar-la, fes doble clic en un nom per canviar-lo.',
|
||||
'journey.studio.layers.group': 'Grup',
|
||||
'journey.studio.layers.groupAction': 'Agrupa',
|
||||
'journey.studio.layers.ungroup': 'Desagrupa',
|
||||
'journey.studio.layers.hide': 'Amaga',
|
||||
'journey.studio.layers.show': 'Mostra',
|
||||
'journey.studio.layers.rename': 'Canvia el nom',
|
||||
'journey.studio.layers.expand': 'Mostra el contingut',
|
||||
'journey.studio.layers.collapse': 'Plega',
|
||||
'journey.studio.layers.align': 'Alinea',
|
||||
'journey.studio.layers.alignLeft': 'Alinea les vores esquerres',
|
||||
'journey.studio.layers.alignCenter': 'Centra horitzontalment',
|
||||
'journey.studio.layers.alignRight': 'Alinea les vores dretes',
|
||||
'journey.studio.layers.alignTop': 'Alinea les vores superiors',
|
||||
'journey.studio.layers.alignMiddle': 'Centra verticalment',
|
||||
'journey.studio.layers.alignBottom': 'Alinea les vores inferiors',
|
||||
'journey.studio.layers.alignTo': 'Respecte a',
|
||||
'journey.studio.layers.toSelection': 'Selecció',
|
||||
'journey.studio.layers.toPage': 'Pàgina',
|
||||
'journey.studio.layers.toSafe': 'Zona segura',
|
||||
'journey.studio.layers.distributeH': 'Reparteix en horitzontal',
|
||||
'journey.studio.layers.distributeV': 'Reparteix en vertical',
|
||||
'journey.studio.layers.copyStyle': 'Copia l’estil',
|
||||
'journey.studio.layers.pasteStyle': 'Enganxa l’estil',
|
||||
'journey.studio.layers.groupHint': 'Fes doble clic en un element per treballar dins del grup.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Vrstvy',
|
||||
'journey.studio.layers.empty': 'Na této stránce zatím nic není. Co přidáte, objeví se tady, odpředu dozadu.',
|
||||
'journey.studio.layers.hint': 'Odpředu dozadu. Přetažením řádku změníte pořadí, dvojklikem na název ho přejmenujete.',
|
||||
'journey.studio.layers.group': 'Skupina',
|
||||
'journey.studio.layers.groupAction': 'Seskupit',
|
||||
'journey.studio.layers.ungroup': 'Zrušit seskupení',
|
||||
'journey.studio.layers.hide': 'Skrýt',
|
||||
'journey.studio.layers.show': 'Zobrazit',
|
||||
'journey.studio.layers.rename': 'Přejmenovat',
|
||||
'journey.studio.layers.expand': 'Zobrazit obsah',
|
||||
'journey.studio.layers.collapse': 'Sbalit',
|
||||
'journey.studio.layers.align': 'Zarovnat',
|
||||
'journey.studio.layers.alignLeft': 'Zarovnat levé okraje',
|
||||
'journey.studio.layers.alignCenter': 'Vystředit vodorovně',
|
||||
'journey.studio.layers.alignRight': 'Zarovnat pravé okraje',
|
||||
'journey.studio.layers.alignTop': 'Zarovnat horní okraje',
|
||||
'journey.studio.layers.alignMiddle': 'Vystředit svisle',
|
||||
'journey.studio.layers.alignBottom': 'Zarovnat dolní okraje',
|
||||
'journey.studio.layers.alignTo': 'Vzhledem k',
|
||||
'journey.studio.layers.toSelection': 'Výběr',
|
||||
'journey.studio.layers.toPage': 'Stránka',
|
||||
'journey.studio.layers.toSafe': 'Bezpečná zóna',
|
||||
'journey.studio.layers.distributeH': 'Rozmístit rovnoměrně vodorovně',
|
||||
'journey.studio.layers.distributeV': 'Rozmístit rovnoměrně svisle',
|
||||
'journey.studio.layers.copyStyle': 'Kopírovat styl',
|
||||
'journey.studio.layers.pasteStyle': 'Vložit styl',
|
||||
'journey.studio.layers.groupHint': 'Dvojklikem na prvek budete pracovat uvnitř skupiny.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Ebenen',
|
||||
'journey.studio.layers.empty':
|
||||
'Auf dieser Seite ist noch nichts. Was du hinzufügst, erscheint hier, von vorn nach hinten.',
|
||||
'journey.studio.layers.hint':
|
||||
'Von vorn nach hinten. Zeile ziehen zum Umsortieren, Namen doppelklicken zum Umbenennen.',
|
||||
'journey.studio.layers.group': 'Gruppe',
|
||||
'journey.studio.layers.groupAction': 'Gruppieren',
|
||||
'journey.studio.layers.ungroup': 'Gruppierung aufheben',
|
||||
'journey.studio.layers.hide': 'Ausblenden',
|
||||
'journey.studio.layers.show': 'Einblenden',
|
||||
'journey.studio.layers.rename': 'Umbenennen',
|
||||
'journey.studio.layers.expand': 'Inhalt zeigen',
|
||||
'journey.studio.layers.collapse': 'Zuklappen',
|
||||
'journey.studio.layers.align': 'Ausrichten',
|
||||
'journey.studio.layers.alignLeft': 'Linke Kanten ausrichten',
|
||||
'journey.studio.layers.alignCenter': 'Horizontal zentrieren',
|
||||
'journey.studio.layers.alignRight': 'Rechte Kanten ausrichten',
|
||||
'journey.studio.layers.alignTop': 'Obere Kanten ausrichten',
|
||||
'journey.studio.layers.alignMiddle': 'Vertikal zentrieren',
|
||||
'journey.studio.layers.alignBottom': 'Untere Kanten ausrichten',
|
||||
'journey.studio.layers.alignTo': 'Bezogen auf',
|
||||
'journey.studio.layers.toSelection': 'Auswahl',
|
||||
'journey.studio.layers.toPage': 'Seite',
|
||||
'journey.studio.layers.toSafe': 'Schutzzone',
|
||||
'journey.studio.layers.distributeH': 'Waagerecht gleichmäßig verteilen',
|
||||
'journey.studio.layers.distributeV': 'Senkrecht gleichmäßig verteilen',
|
||||
'journey.studio.layers.copyStyle': 'Stil kopieren',
|
||||
'journey.studio.layers.pasteStyle': 'Stil einfügen',
|
||||
'journey.studio.layers.groupHint': 'Doppelklicke ein Element, um innerhalb der Gruppe zu arbeiten.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Layers',
|
||||
'journey.studio.layers.empty': 'Nothing on this page yet. Whatever you add appears here, front to back.',
|
||||
'journey.studio.layers.hint': 'Front to back. Drag a row to restack it, double-click a name to rename it.',
|
||||
'journey.studio.layers.group': 'Group',
|
||||
'journey.studio.layers.groupAction': 'Group',
|
||||
'journey.studio.layers.ungroup': 'Ungroup',
|
||||
'journey.studio.layers.hide': 'Hide',
|
||||
'journey.studio.layers.show': 'Show',
|
||||
'journey.studio.layers.rename': 'Rename',
|
||||
'journey.studio.layers.expand': 'Show what is inside',
|
||||
'journey.studio.layers.collapse': 'Fold away',
|
||||
'journey.studio.layers.align': 'Align',
|
||||
'journey.studio.layers.alignLeft': 'Align left edges',
|
||||
'journey.studio.layers.alignCenter': 'Centre horizontally',
|
||||
'journey.studio.layers.alignRight': 'Align right edges',
|
||||
'journey.studio.layers.alignTop': 'Align top edges',
|
||||
'journey.studio.layers.alignMiddle': 'Centre vertically',
|
||||
'journey.studio.layers.alignBottom': 'Align bottom edges',
|
||||
'journey.studio.layers.alignTo': 'Relative to',
|
||||
'journey.studio.layers.toSelection': 'Selection',
|
||||
'journey.studio.layers.toPage': 'Page',
|
||||
'journey.studio.layers.toSafe': 'Safe area',
|
||||
'journey.studio.layers.distributeH': 'Space evenly across',
|
||||
'journey.studio.layers.distributeV': 'Space evenly down',
|
||||
'journey.studio.layers.copyStyle': 'Copy style',
|
||||
'journey.studio.layers.pasteStyle': 'Paste style',
|
||||
'journey.studio.layers.groupHint': 'Double-click an element to work inside the group.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Capas',
|
||||
'journey.studio.layers.empty':
|
||||
'Todavía no hay nada en esta página. Lo que añadas aparece aquí, de delante hacia atrás.',
|
||||
'journey.studio.layers.hint':
|
||||
'De delante hacia atrás. Arrastra una fila para reordenarla, haz doble clic en un nombre para cambiarlo.',
|
||||
'journey.studio.layers.group': 'Grupo',
|
||||
'journey.studio.layers.groupAction': 'Agrupar',
|
||||
'journey.studio.layers.ungroup': 'Desagrupar',
|
||||
'journey.studio.layers.hide': 'Ocultar',
|
||||
'journey.studio.layers.show': 'Mostrar',
|
||||
'journey.studio.layers.rename': 'Cambiar nombre',
|
||||
'journey.studio.layers.expand': 'Mostrar contenido',
|
||||
'journey.studio.layers.collapse': 'Plegar',
|
||||
'journey.studio.layers.align': 'Alinear',
|
||||
'journey.studio.layers.alignLeft': 'Alinear bordes izquierdos',
|
||||
'journey.studio.layers.alignCenter': 'Centrar horizontalmente',
|
||||
'journey.studio.layers.alignRight': 'Alinear bordes derechos',
|
||||
'journey.studio.layers.alignTop': 'Alinear bordes superiores',
|
||||
'journey.studio.layers.alignMiddle': 'Centrar verticalmente',
|
||||
'journey.studio.layers.alignBottom': 'Alinear bordes inferiores',
|
||||
'journey.studio.layers.alignTo': 'Respecto a',
|
||||
'journey.studio.layers.toSelection': 'Selección',
|
||||
'journey.studio.layers.toPage': 'Página',
|
||||
'journey.studio.layers.toSafe': 'Zona segura',
|
||||
'journey.studio.layers.distributeH': 'Repartir en horizontal',
|
||||
'journey.studio.layers.distributeV': 'Repartir en vertical',
|
||||
'journey.studio.layers.copyStyle': 'Copiar estilo',
|
||||
'journey.studio.layers.pasteStyle': 'Pegar estilo',
|
||||
'journey.studio.layers.groupHint': 'Haz doble clic en un elemento para trabajar dentro del grupo.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Kihid',
|
||||
'journey.studio.layers.empty': 'Sellel lehel pole veel midagi. Mida lisad, ilmub siia, eest taha.',
|
||||
'journey.studio.layers.hint': 'Eest taha. Lohista rida järjekorra muutmiseks, topeltklõpsa nimel selle muutmiseks.',
|
||||
'journey.studio.layers.group': 'Rühm',
|
||||
'journey.studio.layers.groupAction': 'Rühmita',
|
||||
'journey.studio.layers.ungroup': 'Tühista rühmitus',
|
||||
'journey.studio.layers.hide': 'Peida',
|
||||
'journey.studio.layers.show': 'Näita',
|
||||
'journey.studio.layers.rename': 'Nimeta ümber',
|
||||
'journey.studio.layers.expand': 'Näita sisu',
|
||||
'journey.studio.layers.collapse': 'Ahenda',
|
||||
'journey.studio.layers.align': 'Joonda',
|
||||
'journey.studio.layers.alignLeft': 'Joonda vasakud servad',
|
||||
'journey.studio.layers.alignCenter': 'Tsentreeri rõhtsalt',
|
||||
'journey.studio.layers.alignRight': 'Joonda paremad servad',
|
||||
'journey.studio.layers.alignTop': 'Joonda ülemised servad',
|
||||
'journey.studio.layers.alignMiddle': 'Tsentreeri püstsalt',
|
||||
'journey.studio.layers.alignBottom': 'Joonda alumised servad',
|
||||
'journey.studio.layers.alignTo': 'Võrreldes',
|
||||
'journey.studio.layers.toSelection': 'Valik',
|
||||
'journey.studio.layers.toPage': 'Leht',
|
||||
'journey.studio.layers.toSafe': 'Turvaala',
|
||||
'journey.studio.layers.distributeH': 'Jaota ühtlaselt rõhtsalt',
|
||||
'journey.studio.layers.distributeV': 'Jaota ühtlaselt püstsalt',
|
||||
'journey.studio.layers.copyStyle': 'Kopeeri stiil',
|
||||
'journey.studio.layers.pasteStyle': 'Kleebi stiil',
|
||||
'journey.studio.layers.groupHint': 'Topeltklõpsa elemendil, et rühma sees töötada.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Calques',
|
||||
'journey.studio.layers.empty':
|
||||
'Rien sur cette page pour l’instant. Ce que vous ajoutez apparaît ici, du premier plan à l’arrière-plan.',
|
||||
'journey.studio.layers.hint':
|
||||
'Du premier plan à l’arrière-plan. Faites glisser une ligne pour la déplacer, double-cliquez sur un nom pour le renommer.',
|
||||
'journey.studio.layers.group': 'Groupe',
|
||||
'journey.studio.layers.groupAction': 'Grouper',
|
||||
'journey.studio.layers.ungroup': 'Dissocier',
|
||||
'journey.studio.layers.hide': 'Masquer',
|
||||
'journey.studio.layers.show': 'Afficher',
|
||||
'journey.studio.layers.rename': 'Renommer',
|
||||
'journey.studio.layers.expand': 'Afficher le contenu',
|
||||
'journey.studio.layers.collapse': 'Replier',
|
||||
'journey.studio.layers.align': 'Aligner',
|
||||
'journey.studio.layers.alignLeft': 'Aligner les bords gauches',
|
||||
'journey.studio.layers.alignCenter': 'Centrer horizontalement',
|
||||
'journey.studio.layers.alignRight': 'Aligner les bords droits',
|
||||
'journey.studio.layers.alignTop': 'Aligner les bords supérieurs',
|
||||
'journey.studio.layers.alignMiddle': 'Centrer verticalement',
|
||||
'journey.studio.layers.alignBottom': 'Aligner les bords inférieurs',
|
||||
'journey.studio.layers.alignTo': 'Par rapport à',
|
||||
'journey.studio.layers.toSelection': 'Sélection',
|
||||
'journey.studio.layers.toPage': 'Page', // same-as-en
|
||||
'journey.studio.layers.toSafe': 'Zone de sécurité',
|
||||
'journey.studio.layers.distributeH': 'Répartir horizontalement',
|
||||
'journey.studio.layers.distributeV': 'Répartir verticalement',
|
||||
'journey.studio.layers.copyStyle': 'Copier le style',
|
||||
'journey.studio.layers.pasteStyle': 'Coller le style',
|
||||
'journey.studio.layers.groupHint': 'Double-cliquez sur un élément pour travailler dans le groupe.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Επίπεδα',
|
||||
'journey.studio.layers.empty':
|
||||
'Δεν υπάρχει τίποτα σε αυτή τη σελίδα ακόμη. Ό,τι προσθέτετε εμφανίζεται εδώ, από μπροστά προς τα πίσω.',
|
||||
'journey.studio.layers.hint':
|
||||
'Από μπροστά προς τα πίσω. Σύρετε μια γραμμή για αλλαγή σειράς, κάντε διπλό κλικ σε ένα όνομα για μετονομασία.',
|
||||
'journey.studio.layers.group': 'Ομάδα',
|
||||
'journey.studio.layers.groupAction': 'Ομαδοποίηση',
|
||||
'journey.studio.layers.ungroup': 'Κατάργηση ομάδας',
|
||||
'journey.studio.layers.hide': 'Απόκρυψη',
|
||||
'journey.studio.layers.show': 'Εμφάνιση',
|
||||
'journey.studio.layers.rename': 'Μετονομασία',
|
||||
'journey.studio.layers.expand': 'Εμφάνιση περιεχομένου',
|
||||
'journey.studio.layers.collapse': 'Σύμπτυξη',
|
||||
'journey.studio.layers.align': 'Στοίχιση',
|
||||
'journey.studio.layers.alignLeft': 'Στοίχιση αριστερών άκρων',
|
||||
'journey.studio.layers.alignCenter': 'Οριζόντιο κεντράρισμα',
|
||||
'journey.studio.layers.alignRight': 'Στοίχιση δεξιών άκρων',
|
||||
'journey.studio.layers.alignTop': 'Στοίχιση πάνω άκρων',
|
||||
'journey.studio.layers.alignMiddle': 'Κατακόρυφο κεντράρισμα',
|
||||
'journey.studio.layers.alignBottom': 'Στοίχιση κάτω άκρων',
|
||||
'journey.studio.layers.alignTo': 'Σε σχέση με',
|
||||
'journey.studio.layers.toSelection': 'Επιλογή',
|
||||
'journey.studio.layers.toPage': 'Σελίδα',
|
||||
'journey.studio.layers.toSafe': 'Ασφαλής ζώνη',
|
||||
'journey.studio.layers.distributeH': 'Ομοιόμορφη κατανομή οριζόντια',
|
||||
'journey.studio.layers.distributeV': 'Ομοιόμορφη κατανομή κατακόρυφα',
|
||||
'journey.studio.layers.copyStyle': 'Αντιγραφή στυλ',
|
||||
'journey.studio.layers.pasteStyle': 'Επικόλληση στυλ',
|
||||
'journey.studio.layers.groupHint': 'Κάντε διπλό κλικ σε ένα στοιχείο για να δουλέψετε μέσα στην ομάδα.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Rétegek',
|
||||
'journey.studio.layers.empty': 'Ezen az oldalon még nincs semmi. Amit hozzáadsz, itt jelenik meg, elölről hátrafelé.',
|
||||
'journey.studio.layers.hint':
|
||||
'Elölről hátrafelé. Húzz egy sort az átrendezéshez, kattints duplán egy névre az átnevezéshez.',
|
||||
'journey.studio.layers.group': 'Csoport',
|
||||
'journey.studio.layers.groupAction': 'Csoportosítás',
|
||||
'journey.studio.layers.ungroup': 'Csoport bontása',
|
||||
'journey.studio.layers.hide': 'Elrejtés',
|
||||
'journey.studio.layers.show': 'Megjelenítés',
|
||||
'journey.studio.layers.rename': 'Átnevezés',
|
||||
'journey.studio.layers.expand': 'Tartalom megjelenítése',
|
||||
'journey.studio.layers.collapse': 'Összecsukás',
|
||||
'journey.studio.layers.align': 'Igazítás',
|
||||
'journey.studio.layers.alignLeft': 'Bal szélek igazítása',
|
||||
'journey.studio.layers.alignCenter': 'Vízszintes középre',
|
||||
'journey.studio.layers.alignRight': 'Jobb szélek igazítása',
|
||||
'journey.studio.layers.alignTop': 'Felső szélek igazítása',
|
||||
'journey.studio.layers.alignMiddle': 'Függőleges középre',
|
||||
'journey.studio.layers.alignBottom': 'Alsó szélek igazítása',
|
||||
'journey.studio.layers.alignTo': 'Viszonyítás',
|
||||
'journey.studio.layers.toSelection': 'Kijelölés',
|
||||
'journey.studio.layers.toPage': 'Oldal',
|
||||
'journey.studio.layers.toSafe': 'Védőzóna',
|
||||
'journey.studio.layers.distributeH': 'Egyenletes elosztás vízszintesen',
|
||||
'journey.studio.layers.distributeV': 'Egyenletes elosztás függőlegesen',
|
||||
'journey.studio.layers.copyStyle': 'Stílus másolása',
|
||||
'journey.studio.layers.pasteStyle': 'Stílus beillesztése',
|
||||
'journey.studio.layers.groupHint': 'Kattints duplán egy elemre, hogy a csoporton belül dolgozz.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Lapisan',
|
||||
'journey.studio.layers.empty':
|
||||
'Belum ada apa pun di halaman ini. Yang kamu tambahkan muncul di sini, dari depan ke belakang.',
|
||||
'journey.studio.layers.hint':
|
||||
'Dari depan ke belakang. Seret baris untuk mengubah urutan, klik dua kali nama untuk mengganti namanya.',
|
||||
'journey.studio.layers.group': 'Grup',
|
||||
'journey.studio.layers.groupAction': 'Kelompokkan',
|
||||
'journey.studio.layers.ungroup': 'Pisahkan grup',
|
||||
'journey.studio.layers.hide': 'Sembunyikan',
|
||||
'journey.studio.layers.show': 'Tampilkan',
|
||||
'journey.studio.layers.rename': 'Ganti nama',
|
||||
'journey.studio.layers.expand': 'Tampilkan isi',
|
||||
'journey.studio.layers.collapse': 'Ciutkan',
|
||||
'journey.studio.layers.align': 'Ratakan',
|
||||
'journey.studio.layers.alignLeft': 'Ratakan tepi kiri',
|
||||
'journey.studio.layers.alignCenter': 'Tengahkan mendatar',
|
||||
'journey.studio.layers.alignRight': 'Ratakan tepi kanan',
|
||||
'journey.studio.layers.alignTop': 'Ratakan tepi atas',
|
||||
'journey.studio.layers.alignMiddle': 'Tengahkan tegak',
|
||||
'journey.studio.layers.alignBottom': 'Ratakan tepi bawah',
|
||||
'journey.studio.layers.alignTo': 'Terhadap',
|
||||
'journey.studio.layers.toSelection': 'Pilihan',
|
||||
'journey.studio.layers.toPage': 'Halaman',
|
||||
'journey.studio.layers.toSafe': 'Area aman',
|
||||
'journey.studio.layers.distributeH': 'Sebar rata mendatar',
|
||||
'journey.studio.layers.distributeV': 'Sebar rata tegak',
|
||||
'journey.studio.layers.copyStyle': 'Salin gaya',
|
||||
'journey.studio.layers.pasteStyle': 'Tempel gaya',
|
||||
'journey.studio.layers.groupHint': 'Klik dua kali sebuah elemen untuk bekerja di dalam grup.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Livelli',
|
||||
'journey.studio.layers.empty':
|
||||
'Ancora niente su questa pagina. Ciò che aggiungi compare qui, dal primo piano allo sfondo.',
|
||||
'journey.studio.layers.hint':
|
||||
'Dal primo piano allo sfondo. Trascina una riga per riordinarla, fai doppio clic su un nome per rinominarlo.',
|
||||
'journey.studio.layers.group': 'Gruppo',
|
||||
'journey.studio.layers.groupAction': 'Raggruppa',
|
||||
'journey.studio.layers.ungroup': 'Separa',
|
||||
'journey.studio.layers.hide': 'Nascondi',
|
||||
'journey.studio.layers.show': 'Mostra',
|
||||
'journey.studio.layers.rename': 'Rinomina',
|
||||
'journey.studio.layers.expand': 'Mostra il contenuto',
|
||||
'journey.studio.layers.collapse': 'Comprimi',
|
||||
'journey.studio.layers.align': 'Allinea',
|
||||
'journey.studio.layers.alignLeft': 'Allinea i bordi sinistri',
|
||||
'journey.studio.layers.alignCenter': 'Centra in orizzontale',
|
||||
'journey.studio.layers.alignRight': 'Allinea i bordi destri',
|
||||
'journey.studio.layers.alignTop': 'Allinea i bordi superiori',
|
||||
'journey.studio.layers.alignMiddle': 'Centra in verticale',
|
||||
'journey.studio.layers.alignBottom': 'Allinea i bordi inferiori',
|
||||
'journey.studio.layers.alignTo': 'Rispetto a',
|
||||
'journey.studio.layers.toSelection': 'Selezione',
|
||||
'journey.studio.layers.toPage': 'Pagina',
|
||||
'journey.studio.layers.toSafe': 'Area di sicurezza',
|
||||
'journey.studio.layers.distributeH': 'Distribuisci in orizzontale',
|
||||
'journey.studio.layers.distributeV': 'Distribuisci in verticale',
|
||||
'journey.studio.layers.copyStyle': 'Copia stile',
|
||||
'journey.studio.layers.pasteStyle': 'Incolla stile',
|
||||
'journey.studio.layers.groupHint': 'Fai doppio clic su un elemento per lavorare dentro il gruppo.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'レイヤー',
|
||||
'journey.studio.layers.empty':
|
||||
'このページにはまだ何もありません。追加したものが前面から背面の順にここに表示されます。',
|
||||
'journey.studio.layers.hint':
|
||||
'前面から背面の順です。行をドラッグして並べ替え、名前をダブルクリックして名前を変更できます。',
|
||||
'journey.studio.layers.group': 'グループ',
|
||||
'journey.studio.layers.groupAction': 'グループ化',
|
||||
'journey.studio.layers.ungroup': 'グループ解除',
|
||||
'journey.studio.layers.hide': '非表示',
|
||||
'journey.studio.layers.show': '表示',
|
||||
'journey.studio.layers.rename': '名前を変更',
|
||||
'journey.studio.layers.expand': '中身を表示',
|
||||
'journey.studio.layers.collapse': '折りたたむ',
|
||||
'journey.studio.layers.align': '整列',
|
||||
'journey.studio.layers.alignLeft': '左端を揃える',
|
||||
'journey.studio.layers.alignCenter': '水平方向の中央に揃える',
|
||||
'journey.studio.layers.alignRight': '右端を揃える',
|
||||
'journey.studio.layers.alignTop': '上端を揃える',
|
||||
'journey.studio.layers.alignMiddle': '垂直方向の中央に揃える',
|
||||
'journey.studio.layers.alignBottom': '下端を揃える',
|
||||
'journey.studio.layers.alignTo': '基準',
|
||||
'journey.studio.layers.toSelection': '選択範囲',
|
||||
'journey.studio.layers.toPage': 'ページ',
|
||||
'journey.studio.layers.toSafe': '安全領域',
|
||||
'journey.studio.layers.distributeH': '左右に均等に配置',
|
||||
'journey.studio.layers.distributeV': '上下に均等に配置',
|
||||
'journey.studio.layers.copyStyle': 'スタイルをコピー',
|
||||
'journey.studio.layers.pasteStyle': 'スタイルを貼り付け',
|
||||
'journey.studio.layers.groupHint': '要素をダブルクリックすると、グループの中で作業できます。',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': '레이어',
|
||||
'journey.studio.layers.empty':
|
||||
'이 페이지에는 아직 아무것도 없습니다. 추가한 항목이 앞에서 뒤 순서로 여기에 표시됩니다.',
|
||||
'journey.studio.layers.hint': '앞에서 뒤 순서입니다. 행을 끌어 순서를 바꾸고, 이름을 두 번 클릭해 이름을 바꿉니다.',
|
||||
'journey.studio.layers.group': '그룹',
|
||||
'journey.studio.layers.groupAction': '그룹화',
|
||||
'journey.studio.layers.ungroup': '그룹 해제',
|
||||
'journey.studio.layers.hide': '숨기기',
|
||||
'journey.studio.layers.show': '보이기',
|
||||
'journey.studio.layers.rename': '이름 바꾸기',
|
||||
'journey.studio.layers.expand': '안에 있는 항목 보기',
|
||||
'journey.studio.layers.collapse': '접기',
|
||||
'journey.studio.layers.align': '정렬',
|
||||
'journey.studio.layers.alignLeft': '왼쪽 가장자리 맞춤',
|
||||
'journey.studio.layers.alignCenter': '가로 가운데 맞춤',
|
||||
'journey.studio.layers.alignRight': '오른쪽 가장자리 맞춤',
|
||||
'journey.studio.layers.alignTop': '위쪽 가장자리 맞춤',
|
||||
'journey.studio.layers.alignMiddle': '세로 가운데 맞춤',
|
||||
'journey.studio.layers.alignBottom': '아래쪽 가장자리 맞춤',
|
||||
'journey.studio.layers.alignTo': '기준',
|
||||
'journey.studio.layers.toSelection': '선택 영역',
|
||||
'journey.studio.layers.toPage': '페이지',
|
||||
'journey.studio.layers.toSafe': '안전 영역',
|
||||
'journey.studio.layers.distributeH': '가로로 균등 분배',
|
||||
'journey.studio.layers.distributeV': '세로로 균등 분배',
|
||||
'journey.studio.layers.copyStyle': '스타일 복사',
|
||||
'journey.studio.layers.pasteStyle': '스타일 붙여넣기',
|
||||
'journey.studio.layers.groupHint': '요소를 두 번 클릭하면 그룹 안에서 작업할 수 있습니다.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Lagen',
|
||||
'journey.studio.layers.empty': 'Nog niets op deze pagina. Wat je toevoegt, verschijnt hier, van voor naar achter.',
|
||||
'journey.studio.layers.hint':
|
||||
'Van voor naar achter. Sleep een rij om de volgorde te wijzigen, dubbelklik op een naam om die te wijzigen.',
|
||||
'journey.studio.layers.group': 'Groep',
|
||||
'journey.studio.layers.groupAction': 'Groeperen',
|
||||
'journey.studio.layers.ungroup': 'Groep opheffen',
|
||||
'journey.studio.layers.hide': 'Verbergen',
|
||||
'journey.studio.layers.show': 'Tonen',
|
||||
'journey.studio.layers.rename': 'Naam wijzigen',
|
||||
'journey.studio.layers.expand': 'Inhoud tonen',
|
||||
'journey.studio.layers.collapse': 'Inklappen',
|
||||
'journey.studio.layers.align': 'Uitlijnen',
|
||||
'journey.studio.layers.alignLeft': 'Linkerranden uitlijnen',
|
||||
'journey.studio.layers.alignCenter': 'Horizontaal centreren',
|
||||
'journey.studio.layers.alignRight': 'Rechterranden uitlijnen',
|
||||
'journey.studio.layers.alignTop': 'Bovenranden uitlijnen',
|
||||
'journey.studio.layers.alignMiddle': 'Verticaal centreren',
|
||||
'journey.studio.layers.alignBottom': 'Onderranden uitlijnen',
|
||||
'journey.studio.layers.alignTo': 'Ten opzichte van',
|
||||
'journey.studio.layers.toSelection': 'Selectie',
|
||||
'journey.studio.layers.toPage': 'Pagina',
|
||||
'journey.studio.layers.toSafe': 'Veilige zone',
|
||||
'journey.studio.layers.distributeH': 'Horizontaal gelijk verdelen',
|
||||
'journey.studio.layers.distributeV': 'Verticaal gelijk verdelen',
|
||||
'journey.studio.layers.copyStyle': 'Stijl kopiëren',
|
||||
'journey.studio.layers.pasteStyle': 'Stijl plakken',
|
||||
'journey.studio.layers.groupHint': 'Dubbelklik op een element om binnen de groep te werken.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Warstwy',
|
||||
'journey.studio.layers.empty':
|
||||
'Na tej stronie nic jeszcze nie ma. To, co dodasz, pojawi się tutaj, od przodu do tyłu.',
|
||||
'journey.studio.layers.hint':
|
||||
'Od przodu do tyłu. Przeciągnij wiersz, aby zmienić kolejność, kliknij dwukrotnie nazwę, aby ją zmienić.',
|
||||
'journey.studio.layers.group': 'Grupa',
|
||||
'journey.studio.layers.groupAction': 'Grupuj',
|
||||
'journey.studio.layers.ungroup': 'Rozgrupuj',
|
||||
'journey.studio.layers.hide': 'Ukryj',
|
||||
'journey.studio.layers.show': 'Pokaż',
|
||||
'journey.studio.layers.rename': 'Zmień nazwę',
|
||||
'journey.studio.layers.expand': 'Pokaż zawartość',
|
||||
'journey.studio.layers.collapse': 'Zwiń',
|
||||
'journey.studio.layers.align': 'Wyrównaj',
|
||||
'journey.studio.layers.alignLeft': 'Wyrównaj lewe krawędzie',
|
||||
'journey.studio.layers.alignCenter': 'Wyśrodkuj w poziomie',
|
||||
'journey.studio.layers.alignRight': 'Wyrównaj prawe krawędzie',
|
||||
'journey.studio.layers.alignTop': 'Wyrównaj górne krawędzie',
|
||||
'journey.studio.layers.alignMiddle': 'Wyśrodkuj w pionie',
|
||||
'journey.studio.layers.alignBottom': 'Wyrównaj dolne krawędzie',
|
||||
'journey.studio.layers.alignTo': 'Względem',
|
||||
'journey.studio.layers.toSelection': 'Zaznaczenie',
|
||||
'journey.studio.layers.toPage': 'Strona',
|
||||
'journey.studio.layers.toSafe': 'Strefa bezpieczna',
|
||||
'journey.studio.layers.distributeH': 'Rozmieść równo w poziomie',
|
||||
'journey.studio.layers.distributeV': 'Rozmieść równo w pionie',
|
||||
'journey.studio.layers.copyStyle': 'Kopiuj styl',
|
||||
'journey.studio.layers.pasteStyle': 'Wklej styl',
|
||||
'journey.studio.layers.groupHint': 'Kliknij dwukrotnie element, aby pracować wewnątrz grupy.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Слои',
|
||||
'journey.studio.layers.empty':
|
||||
'На этой странице пока ничего нет. Всё, что вы добавите, появится здесь, спереди назад.',
|
||||
'journey.studio.layers.hint':
|
||||
'Спереди назад. Перетащите строку, чтобы изменить порядок, дважды щёлкните имя, чтобы переименовать.',
|
||||
'journey.studio.layers.group': 'Группа',
|
||||
'journey.studio.layers.groupAction': 'Сгруппировать',
|
||||
'journey.studio.layers.ungroup': 'Разгруппировать',
|
||||
'journey.studio.layers.hide': 'Скрыть',
|
||||
'journey.studio.layers.show': 'Показать',
|
||||
'journey.studio.layers.rename': 'Переименовать',
|
||||
'journey.studio.layers.expand': 'Показать содержимое',
|
||||
'journey.studio.layers.collapse': 'Свернуть',
|
||||
'journey.studio.layers.align': 'Выравнивание',
|
||||
'journey.studio.layers.alignLeft': 'Выровнять по левому краю',
|
||||
'journey.studio.layers.alignCenter': 'Центрировать по горизонтали',
|
||||
'journey.studio.layers.alignRight': 'Выровнять по правому краю',
|
||||
'journey.studio.layers.alignTop': 'Выровнять по верхнему краю',
|
||||
'journey.studio.layers.alignMiddle': 'Центрировать по вертикали',
|
||||
'journey.studio.layers.alignBottom': 'Выровнять по нижнему краю',
|
||||
'journey.studio.layers.alignTo': 'Относительно',
|
||||
'journey.studio.layers.toSelection': 'Выделение',
|
||||
'journey.studio.layers.toPage': 'Страница',
|
||||
'journey.studio.layers.toSafe': 'Безопасная зона',
|
||||
'journey.studio.layers.distributeH': 'Распределить по горизонтали',
|
||||
'journey.studio.layers.distributeV': 'Распределить по вертикали',
|
||||
'journey.studio.layers.copyStyle': 'Копировать стиль',
|
||||
'journey.studio.layers.pasteStyle': 'Вставить стиль',
|
||||
'journey.studio.layers.groupHint': 'Дважды щёлкните элемент, чтобы работать внутри группы.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Vrstvy',
|
||||
'journey.studio.layers.empty': 'Na tejto stránke zatiaľ nič nie je. Čo pridáte, objaví sa tu, spredu dozadu.',
|
||||
'journey.studio.layers.hint':
|
||||
'Spredu dozadu. Potiahnutím riadka zmeníte poradie, dvojklikom na názov ho premenujete.',
|
||||
'journey.studio.layers.group': 'Skupina',
|
||||
'journey.studio.layers.groupAction': 'Zoskupiť',
|
||||
'journey.studio.layers.ungroup': 'Zrušiť zoskupenie',
|
||||
'journey.studio.layers.hide': 'Skryť',
|
||||
'journey.studio.layers.show': 'Zobraziť',
|
||||
'journey.studio.layers.rename': 'Premenovať',
|
||||
'journey.studio.layers.expand': 'Zobraziť obsah',
|
||||
'journey.studio.layers.collapse': 'Zbaliť',
|
||||
'journey.studio.layers.align': 'Zarovnať',
|
||||
'journey.studio.layers.alignLeft': 'Zarovnať ľavé okraje',
|
||||
'journey.studio.layers.alignCenter': 'Vycentrovať vodorovne',
|
||||
'journey.studio.layers.alignRight': 'Zarovnať pravé okraje',
|
||||
'journey.studio.layers.alignTop': 'Zarovnať horné okraje',
|
||||
'journey.studio.layers.alignMiddle': 'Vycentrovať zvisle',
|
||||
'journey.studio.layers.alignBottom': 'Zarovnať dolné okraje',
|
||||
'journey.studio.layers.alignTo': 'Vzhľadom na',
|
||||
'journey.studio.layers.toSelection': 'Výber',
|
||||
'journey.studio.layers.toPage': 'Stránka',
|
||||
'journey.studio.layers.toSafe': 'Bezpečná zóna',
|
||||
'journey.studio.layers.distributeH': 'Rozmiestniť rovnomerne vodorovne',
|
||||
'journey.studio.layers.distributeV': 'Rozmiestniť rovnomerne zvisle',
|
||||
'journey.studio.layers.copyStyle': 'Kopírovať štýl',
|
||||
'journey.studio.layers.pasteStyle': 'Vložiť štýl',
|
||||
'journey.studio.layers.groupHint': 'Dvojklikom na prvok budete pracovať vnútri skupiny.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Lager',
|
||||
'journey.studio.layers.empty': 'Inget på den här sidan än. Det du lägger till visas här, framifrån och bakåt.',
|
||||
'journey.studio.layers.hint':
|
||||
'Framifrån och bakåt. Dra en rad för att ändra ordningen, dubbelklicka på ett namn för att byta det.',
|
||||
'journey.studio.layers.group': 'Grupp',
|
||||
'journey.studio.layers.groupAction': 'Gruppera',
|
||||
'journey.studio.layers.ungroup': 'Dela upp grupp',
|
||||
'journey.studio.layers.hide': 'Dölj',
|
||||
'journey.studio.layers.show': 'Visa',
|
||||
'journey.studio.layers.rename': 'Byt namn',
|
||||
'journey.studio.layers.expand': 'Visa innehållet',
|
||||
'journey.studio.layers.collapse': 'Fäll ihop',
|
||||
'journey.studio.layers.align': 'Justera',
|
||||
'journey.studio.layers.alignLeft': 'Justera vänsterkanter',
|
||||
'journey.studio.layers.alignCenter': 'Centrera vågrätt',
|
||||
'journey.studio.layers.alignRight': 'Justera högerkanter',
|
||||
'journey.studio.layers.alignTop': 'Justera överkanter',
|
||||
'journey.studio.layers.alignMiddle': 'Centrera lodrätt',
|
||||
'journey.studio.layers.alignBottom': 'Justera underkanter',
|
||||
'journey.studio.layers.alignTo': 'I förhållande till',
|
||||
'journey.studio.layers.toSelection': 'Markering',
|
||||
'journey.studio.layers.toPage': 'Sida',
|
||||
'journey.studio.layers.toSafe': 'Skyddszon',
|
||||
'journey.studio.layers.distributeH': 'Fördela jämnt vågrätt',
|
||||
'journey.studio.layers.distributeV': 'Fördela jämnt lodrätt',
|
||||
'journey.studio.layers.copyStyle': 'Kopiera stil',
|
||||
'journey.studio.layers.pasteStyle': 'Klistra in stil',
|
||||
'journey.studio.layers.groupHint': 'Dubbelklicka på ett element för att arbeta inuti gruppen.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'เลเยอร์',
|
||||
'journey.studio.layers.empty': 'หน้านี้ยังไม่มีอะไร สิ่งที่คุณเพิ่มจะแสดงที่นี่ จากหน้าไปหลัง',
|
||||
'journey.studio.layers.hint': 'จากหน้าไปหลัง ลากแถวเพื่อจัดลำดับใหม่ ดับเบิลคลิกที่ชื่อเพื่อเปลี่ยนชื่อ',
|
||||
'journey.studio.layers.group': 'กลุ่ม',
|
||||
'journey.studio.layers.groupAction': 'จัดกลุ่ม',
|
||||
'journey.studio.layers.ungroup': 'ยกเลิกกลุ่ม',
|
||||
'journey.studio.layers.hide': 'ซ่อน',
|
||||
'journey.studio.layers.show': 'แสดง',
|
||||
'journey.studio.layers.rename': 'เปลี่ยนชื่อ',
|
||||
'journey.studio.layers.expand': 'แสดงสิ่งที่อยู่ข้างใน',
|
||||
'journey.studio.layers.collapse': 'ยุบ',
|
||||
'journey.studio.layers.align': 'จัดแนว',
|
||||
'journey.studio.layers.alignLeft': 'จัดขอบซ้ายให้ตรงกัน',
|
||||
'journey.studio.layers.alignCenter': 'จัดกึ่งกลางแนวนอน',
|
||||
'journey.studio.layers.alignRight': 'จัดขอบขวาให้ตรงกัน',
|
||||
'journey.studio.layers.alignTop': 'จัดขอบบนให้ตรงกัน',
|
||||
'journey.studio.layers.alignMiddle': 'จัดกึ่งกลางแนวตั้ง',
|
||||
'journey.studio.layers.alignBottom': 'จัดขอบล่างให้ตรงกัน',
|
||||
'journey.studio.layers.alignTo': 'เทียบกับ',
|
||||
'journey.studio.layers.toSelection': 'ส่วนที่เลือก',
|
||||
'journey.studio.layers.toPage': 'หน้า',
|
||||
'journey.studio.layers.toSafe': 'พื้นที่ปลอดภัย',
|
||||
'journey.studio.layers.distributeH': 'กระจายเท่ากันแนวนอน',
|
||||
'journey.studio.layers.distributeV': 'กระจายเท่ากันแนวตั้ง',
|
||||
'journey.studio.layers.copyStyle': 'คัดลอกสไตล์',
|
||||
'journey.studio.layers.pasteStyle': 'วางสไตล์',
|
||||
'journey.studio.layers.groupHint': 'ดับเบิลคลิกที่องค์ประกอบเพื่อทำงานภายในกลุ่ม',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Katmanlar',
|
||||
'journey.studio.layers.empty': 'Bu sayfada henüz bir şey yok. Eklediğin her şey burada, önden arkaya görünür.',
|
||||
'journey.studio.layers.hint':
|
||||
'Önden arkaya. Sırayı değiştirmek için bir satırı sürükle, yeniden adlandırmak için bir ada çift tıkla.',
|
||||
'journey.studio.layers.group': 'Grup',
|
||||
'journey.studio.layers.groupAction': 'Grupla',
|
||||
'journey.studio.layers.ungroup': 'Grubu çöz',
|
||||
'journey.studio.layers.hide': 'Gizle',
|
||||
'journey.studio.layers.show': 'Göster',
|
||||
'journey.studio.layers.rename': 'Yeniden adlandır',
|
||||
'journey.studio.layers.expand': 'İçeriği göster',
|
||||
'journey.studio.layers.collapse': 'Daralt',
|
||||
'journey.studio.layers.align': 'Hizala',
|
||||
'journey.studio.layers.alignLeft': 'Sol kenarları hizala',
|
||||
'journey.studio.layers.alignCenter': 'Yatay ortala',
|
||||
'journey.studio.layers.alignRight': 'Sağ kenarları hizala',
|
||||
'journey.studio.layers.alignTop': 'Üst kenarları hizala',
|
||||
'journey.studio.layers.alignMiddle': 'Dikey ortala',
|
||||
'journey.studio.layers.alignBottom': 'Alt kenarları hizala',
|
||||
'journey.studio.layers.alignTo': 'Göre',
|
||||
'journey.studio.layers.toSelection': 'Seçim',
|
||||
'journey.studio.layers.toPage': 'Sayfa',
|
||||
'journey.studio.layers.toSafe': 'Güvenli alan',
|
||||
'journey.studio.layers.distributeH': 'Yatayda eşit dağıt',
|
||||
'journey.studio.layers.distributeV': 'Dikeyde eşit dağıt',
|
||||
'journey.studio.layers.copyStyle': 'Stili kopyala',
|
||||
'journey.studio.layers.pasteStyle': 'Stili yapıştır',
|
||||
'journey.studio.layers.groupHint': 'Grubun içinde çalışmak için bir öğeye çift tıkla.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Шари',
|
||||
'journey.studio.layers.empty': 'На цій сторінці ще нічого немає. Усе, що ви додасте, з’явиться тут, спереду назад.',
|
||||
'journey.studio.layers.hint':
|
||||
'Спереду назад. Перетягніть рядок, щоб змінити порядок, двічі клацніть назву, щоб перейменувати.',
|
||||
'journey.studio.layers.group': 'Група',
|
||||
'journey.studio.layers.groupAction': 'Згрупувати',
|
||||
'journey.studio.layers.ungroup': 'Розгрупувати',
|
||||
'journey.studio.layers.hide': 'Сховати',
|
||||
'journey.studio.layers.show': 'Показати',
|
||||
'journey.studio.layers.rename': 'Перейменувати',
|
||||
'journey.studio.layers.expand': 'Показати вміст',
|
||||
'journey.studio.layers.collapse': 'Згорнути',
|
||||
'journey.studio.layers.align': 'Вирівнювання',
|
||||
'journey.studio.layers.alignLeft': 'Вирівняти за лівим краєм',
|
||||
'journey.studio.layers.alignCenter': 'Центрувати по горизонталі',
|
||||
'journey.studio.layers.alignRight': 'Вирівняти за правим краєм',
|
||||
'journey.studio.layers.alignTop': 'Вирівняти за верхнім краєм',
|
||||
'journey.studio.layers.alignMiddle': 'Центрувати по вертикалі',
|
||||
'journey.studio.layers.alignBottom': 'Вирівняти за нижнім краєм',
|
||||
'journey.studio.layers.alignTo': 'Відносно',
|
||||
'journey.studio.layers.toSelection': 'Виділення',
|
||||
'journey.studio.layers.toPage': 'Сторінка',
|
||||
'journey.studio.layers.toSafe': 'Безпечна зона',
|
||||
'journey.studio.layers.distributeH': 'Розподілити по горизонталі',
|
||||
'journey.studio.layers.distributeV': 'Розподілити по вертикалі',
|
||||
'journey.studio.layers.copyStyle': 'Копіювати стиль',
|
||||
'journey.studio.layers.pasteStyle': 'Вставити стиль',
|
||||
'journey.studio.layers.groupHint': 'Двічі клацніть елемент, щоб працювати всередині групи.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': 'Lớp',
|
||||
'journey.studio.layers.empty': 'Trang này chưa có gì. Những gì bạn thêm sẽ hiện ở đây, từ trước ra sau.',
|
||||
'journey.studio.layers.hint': 'Từ trước ra sau. Kéo một hàng để sắp xếp lại, nhấp đúp vào tên để đổi tên.',
|
||||
'journey.studio.layers.group': 'Nhóm',
|
||||
'journey.studio.layers.groupAction': 'Nhóm lại',
|
||||
'journey.studio.layers.ungroup': 'Bỏ nhóm',
|
||||
'journey.studio.layers.hide': 'Ẩn',
|
||||
'journey.studio.layers.show': 'Hiện',
|
||||
'journey.studio.layers.rename': 'Đổi tên',
|
||||
'journey.studio.layers.expand': 'Hiện nội dung',
|
||||
'journey.studio.layers.collapse': 'Thu gọn',
|
||||
'journey.studio.layers.align': 'Căn chỉnh',
|
||||
'journey.studio.layers.alignLeft': 'Căn mép trái',
|
||||
'journey.studio.layers.alignCenter': 'Căn giữa theo chiều ngang',
|
||||
'journey.studio.layers.alignRight': 'Căn mép phải',
|
||||
'journey.studio.layers.alignTop': 'Căn mép trên',
|
||||
'journey.studio.layers.alignMiddle': 'Căn giữa theo chiều dọc',
|
||||
'journey.studio.layers.alignBottom': 'Căn mép dưới',
|
||||
'journey.studio.layers.alignTo': 'So với',
|
||||
'journey.studio.layers.toSelection': 'Vùng chọn',
|
||||
'journey.studio.layers.toPage': 'Trang',
|
||||
'journey.studio.layers.toSafe': 'Vùng an toàn',
|
||||
'journey.studio.layers.distributeH': 'Dàn đều theo chiều ngang',
|
||||
'journey.studio.layers.distributeV': 'Dàn đều theo chiều dọc',
|
||||
'journey.studio.layers.copyStyle': 'Sao chép kiểu',
|
||||
'journey.studio.layers.pasteStyle': 'Dán kiểu',
|
||||
'journey.studio.layers.groupHint': 'Nhấp đúp vào một phần tử để làm việc bên trong nhóm.',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': '圖層',
|
||||
'journey.studio.layers.empty': '此頁面還沒有內容。你新增的內容會依由前到後的順序顯示在這裡。',
|
||||
'journey.studio.layers.hint': '由前到後排列。拖曳一列可調整順序,按兩下名稱可重新命名。',
|
||||
'journey.studio.layers.group': '群組',
|
||||
'journey.studio.layers.groupAction': '組成群組',
|
||||
'journey.studio.layers.ungroup': '取消群組',
|
||||
'journey.studio.layers.hide': '隱藏',
|
||||
'journey.studio.layers.show': '顯示',
|
||||
'journey.studio.layers.rename': '重新命名',
|
||||
'journey.studio.layers.expand': '顯示群組內容',
|
||||
'journey.studio.layers.collapse': '收合',
|
||||
'journey.studio.layers.align': '對齊',
|
||||
'journey.studio.layers.alignLeft': '左邊緣對齊',
|
||||
'journey.studio.layers.alignCenter': '水平置中',
|
||||
'journey.studio.layers.alignRight': '右邊緣對齊',
|
||||
'journey.studio.layers.alignTop': '上邊緣對齊',
|
||||
'journey.studio.layers.alignMiddle': '垂直置中',
|
||||
'journey.studio.layers.alignBottom': '下邊緣對齊',
|
||||
'journey.studio.layers.alignTo': '相對於',
|
||||
'journey.studio.layers.toSelection': '選取範圍',
|
||||
'journey.studio.layers.toPage': '頁面',
|
||||
'journey.studio.layers.toSafe': '安全區',
|
||||
'journey.studio.layers.distributeH': '水平平均分布',
|
||||
'journey.studio.layers.distributeV': '垂直平均分布',
|
||||
'journey.studio.layers.copyStyle': '複製樣式',
|
||||
'journey.studio.layers.pasteStyle': '貼上樣式',
|
||||
'journey.studio.layers.groupHint': '按兩下某個元素即可在群組內編輯。',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -61,6 +61,7 @@ import storage from './storage';
|
||||
import studioFonts from './studioFonts';
|
||||
import studioPaint from './studioPaint';
|
||||
import studioStickers from './studioStickers';
|
||||
import studioLayers from './studioLayers';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import tourCollection from './tourCollection';
|
||||
@@ -150,5 +151,6 @@ const locale = {
|
||||
...assistantVacay,
|
||||
...assistantJourney,
|
||||
...assistantPacking,
|
||||
...studioLayers,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
/** The Studio's layers panel, groups, align and the style clipboard. */
|
||||
const studioLayers: TranslationStrings = {
|
||||
'journey.studio.layers': '图层',
|
||||
'journey.studio.layers.empty': '此页面还没有内容。你添加的内容会按从前到后的顺序显示在这里。',
|
||||
'journey.studio.layers.hint': '从前到后排列。拖动一行可调整顺序,双击名称可重命名。',
|
||||
'journey.studio.layers.group': '组',
|
||||
'journey.studio.layers.groupAction': '编组',
|
||||
'journey.studio.layers.ungroup': '取消编组',
|
||||
'journey.studio.layers.hide': '隐藏',
|
||||
'journey.studio.layers.show': '显示',
|
||||
'journey.studio.layers.rename': '重命名',
|
||||
'journey.studio.layers.expand': '显示组内内容',
|
||||
'journey.studio.layers.collapse': '折叠',
|
||||
'journey.studio.layers.align': '对齐',
|
||||
'journey.studio.layers.alignLeft': '左边缘对齐',
|
||||
'journey.studio.layers.alignCenter': '水平居中',
|
||||
'journey.studio.layers.alignRight': '右边缘对齐',
|
||||
'journey.studio.layers.alignTop': '上边缘对齐',
|
||||
'journey.studio.layers.alignMiddle': '垂直居中',
|
||||
'journey.studio.layers.alignBottom': '下边缘对齐',
|
||||
'journey.studio.layers.alignTo': '相对于',
|
||||
'journey.studio.layers.toSelection': '所选内容',
|
||||
'journey.studio.layers.toPage': '页面',
|
||||
'journey.studio.layers.toSafe': '安全区',
|
||||
'journey.studio.layers.distributeH': '水平均匀分布',
|
||||
'journey.studio.layers.distributeV': '垂直均匀分布',
|
||||
'journey.studio.layers.copyStyle': '复制样式',
|
||||
'journey.studio.layers.pasteStyle': '粘贴样式',
|
||||
'journey.studio.layers.groupHint': '双击某个元素即可在组内编辑。',
|
||||
};
|
||||
|
||||
export default studioLayers;
|
||||
@@ -157,6 +157,10 @@ export * from './book/book-colour';
|
||||
export * from './book/book-paint.schema';
|
||||
export * from './book/book-paint';
|
||||
export * from './book/book-findings-design';
|
||||
export * from './book/book-groups';
|
||||
export * from './book/book-arrange';
|
||||
export * from './book/book-group-ops.schema';
|
||||
export * from './book/book-style-copy';
|
||||
export * from './book/auto/auto-types';
|
||||
export * from './book/auto/auto-book';
|
||||
export * from './book/auto/auto-templates.data';
|
||||
|
||||
Reference in New Issue
Block a user