mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
bdf5767a5f
The rail lists its sections in the order a book is made: Pages, Content, Layouts, Elements and Travel, then Theme and Layers set apart below a rule. Stickers are a tab of Elements beside the icons instead of a rail section of their own, and Elements has six tabs (text, shapes, lines, frames with the grids, icons, stickers). The quotation mark moved to the text styles, the outline rectangle to the basic shapes and the plain rule to the lines. The rail and every tab row are tab lists the arrow keys walk through, and a long name wraps instead of being cut. The icon and sticker searches say in the field how many match, which the sticker library's own head used to. Every panel on both sides opens on the same head, and every labelled control uses one row that moves the control under its label when the two do not fit, so no label sits in a fixed column any more. Number fields, chips, sliders, tab labels, theme names and travel cards wrap the same way, a country beside a stop wraps under itself, and an upload cell holds its words to two lines with the whole label in its tooltip. The heads, shelves and groups are real headings, so a screen reader can walk the panels by them. The inspector shows every kind in one order: what the element is and says, its position and size, its look, its effects, then Arrange. Align and the stacking buttons are one Arrange group with a button that opens the page in Layers; its rows sit under their labels, each in two sets that move to a second line whole. Rarely used groups start folded and open on an element that already uses them (a turned element, a map held at a zoom, a summary set in another face), and every group mounts afresh for the next selection. A sticker's duplicate angle and opacity are gone in favour of the shared position group, which now has the turn buttons for every kind. With several elements selected it keeps their angle, opacity and turn, and the mirror when all of them are stickers, and leaves the frame's numbers to one element. A photo's fill page and fill spread sit under its crop. With nothing selected the inspector shows the page background, the book's theme with the way to the Theme panel, and the page numbers. Seventeen translation keys nothing has used since before the rebuild (the old inspector title, the empty spread note, the summary and country list names, the roads buttons and others) are gone from every language. The broad key template the old panels used had counted them as reached; the format names, the empty photo grid messages and the map source hints are now named where the key check can read them. Also fixed on the way: the travel stops list shared its class with the gradient stops and was laid out as sixteen pixel handles; the alignment buttons were named in raw English; the map's print resolution note joined its two halves with a dash.
236 lines
9.0 KiB
TypeScript
236 lines
9.0 KiB
TypeScript
import type { BookAlignEdge, BookAlignTo, BookSpread } from '@trek/shared';
|
|
import { distributableCount, isWholeGroup } from '@trek/shared';
|
|
import {
|
|
AlignCenterHorizontal,
|
|
AlignCenterVertical,
|
|
AlignEndHorizontal,
|
|
AlignEndVertical,
|
|
AlignHorizontalDistributeCenter,
|
|
AlignStartHorizontal,
|
|
AlignStartVertical,
|
|
AlignVerticalDistributeCenter,
|
|
ArrowDown,
|
|
ArrowUp,
|
|
ChevronsDown,
|
|
ChevronsUp,
|
|
Group,
|
|
Layers,
|
|
Paintbrush,
|
|
Pipette,
|
|
Ungroup,
|
|
type LucideIcon,
|
|
} from 'lucide-react';
|
|
import { useState } from 'react';
|
|
|
|
import { useStudioStore } from '../../store/studioStore';
|
|
import '../../styles/studio-panels.css';
|
|
import { Tooltip } from '../shared/Tooltip';
|
|
import { Choice, Line, Section } from './StudioControls';
|
|
import { useStudioShortcut } from './studioShortcut';
|
|
|
|
/**
|
|
* Arrange: where the selection sits among the other things on the page.
|
|
*
|
|
* Lining it up and spacing it out, what it lines up against, what lies in
|
|
* front of what, grouping, and the style clipboard, in one group at the foot
|
|
* of the inspector. These were two groups, Align and Arrange, one under the
|
|
* other and both about the selection's place among its neighbours, so the
|
|
* stacking buttons were a fold away from the alignment they are used with.
|
|
*
|
|
* Every line puts its row under its label, across the whole width of the
|
|
* panel: the six edges and the two spacings are eight buttons, more than fit
|
|
* beside a label. Each row is two sets, the edges and the spacings, the stack
|
|
* and the grouping, and a set that no longer fits moves to the next line
|
|
* whole, so a narrow panel shows two even rows rather than a ragged one.
|
|
*
|
|
* The align row lines a multi-selection up within its own box by default, and
|
|
* a single element (or one whole group) against its page, since there is
|
|
* nothing else for it to line up with. Groups move as one box throughout
|
|
* (shared/src/book/book-arrange.ts). The Layers button opens the rail's
|
|
* Layers panel, the whole stack of the page beside the selection. Every press
|
|
* is one undo step.
|
|
*/
|
|
|
|
const ALIGN: { edge: BookAlignEdge; icon: LucideIcon; labelKey: string }[] = [
|
|
{ edge: 'left', icon: AlignStartVertical, labelKey: 'journey.studio.layers.alignLeft' },
|
|
{ edge: 'center', icon: AlignCenterVertical, labelKey: 'journey.studio.layers.alignCenter' },
|
|
{ edge: 'right', icon: AlignEndVertical, labelKey: 'journey.studio.layers.alignRight' },
|
|
{ edge: 'top', icon: AlignStartHorizontal, labelKey: 'journey.studio.layers.alignTop' },
|
|
{ edge: 'middle', icon: AlignCenterHorizontal, labelKey: 'journey.studio.layers.alignMiddle' },
|
|
{ edge: 'bottom', icon: AlignEndHorizontal, labelKey: 'journey.studio.layers.alignBottom' },
|
|
];
|
|
|
|
const STACK: { to: 'front' | 'up' | 'down' | 'back'; icon: LucideIcon; labelKey: string }[] = [
|
|
{ to: 'front', icon: ChevronsUp, labelKey: 'journey.studio.toFront' },
|
|
{ to: 'up', icon: ArrowUp, labelKey: 'journey.studio.forward' },
|
|
{ to: 'down', icon: ArrowDown, labelKey: 'journey.studio.backward' },
|
|
{ to: 'back', icon: ChevronsDown, labelKey: 'journey.studio.toBack' },
|
|
];
|
|
|
|
/** How many things move as one in a selection: a group counts once. */
|
|
function unitCount(spread: BookSpread, ids: string[]): number {
|
|
return distributableCount(
|
|
spread,
|
|
spread.elements.filter((e) => ids.includes(e.id))
|
|
);
|
|
}
|
|
|
|
function IconButton({
|
|
label,
|
|
icon: Icon,
|
|
onClick,
|
|
disabled,
|
|
shortcut,
|
|
}: {
|
|
label: string;
|
|
icon: LucideIcon;
|
|
onClick: () => void;
|
|
disabled?: boolean;
|
|
shortcut?: readonly string[];
|
|
}) {
|
|
return (
|
|
<Tooltip label={label} shortcut={shortcut}>
|
|
<button type="button" className="st-chip is-icon" onClick={onClick} disabled={disabled} aria-label={label}>
|
|
<Icon size={14} />
|
|
</button>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
export function StudioArrangeSection({
|
|
spreadIndex,
|
|
t,
|
|
onShowLayers,
|
|
}: {
|
|
spreadIndex: number;
|
|
t: (k: string) => string;
|
|
/** Opens the Layers panel in the rail; without it the button is left out. */
|
|
onShowLayers?: () => void;
|
|
}) {
|
|
const spread = useStudioStore((s) => s.doc?.spreads[spreadIndex]);
|
|
const selection = useStudioStore((s) => s.selection);
|
|
const hasStyle = useStudioStore((s) => s.styleClip !== null);
|
|
const align = useStudioStore((s) => s.align);
|
|
const distribute = useStudioStore((s) => s.distribute);
|
|
const raise = useStudioStore((s) => s.raise);
|
|
const group = useStudioStore((s) => s.group);
|
|
const ungroup = useStudioStore((s) => s.ungroup);
|
|
const copyStyle = useStudioStore((s) => s.copyStyle);
|
|
const pasteStyle = useStudioStore((s) => s.pasteStyle);
|
|
const keys = useStudioShortcut();
|
|
const [chosenTo, setChosenTo] = useState<BookAlignTo | null>(null);
|
|
|
|
if (!spread || !selection.length) return null;
|
|
const sel = spread.elements.filter((e) => selection.includes(e.id));
|
|
const movable = sel.filter((e) => !e.locked).map((e) => e.id);
|
|
const units = unitCount(spread, movable);
|
|
// One thing has nothing to line up with but its page.
|
|
const to: BookAlignTo = units < 2 ? (chosenTo === 'safe' ? 'safe' : 'page') : (chosenTo ?? 'selection');
|
|
const grouped = sel.some((e) => e.group);
|
|
const canGroup = movable.length >= 2 && !isWholeGroup(spread, movable);
|
|
const asGroup = selection.length > 1 && isWholeGroup(spread, selection);
|
|
|
|
return (
|
|
// Open for a multi-selection, where lining up is what comes next; folded for one element.
|
|
<Section key={units > 1 ? 'many' : 'one'} label={t('journey.studio.arrange')} defaultOpen={units > 1}>
|
|
<div className="st-arrange">
|
|
<Line label={t('journey.studio.layers.align')} wide>
|
|
<div className="st-row st-arrange-row">
|
|
<span className="st-arrange-set">
|
|
{ALIGN.map((a) => (
|
|
<IconButton
|
|
key={a.edge}
|
|
label={t(a.labelKey)}
|
|
icon={a.icon}
|
|
disabled={!movable.length}
|
|
onClick={() => align(spreadIndex, a.edge, to)}
|
|
/>
|
|
))}
|
|
</span>
|
|
<span className="st-arrange-set">
|
|
<IconButton
|
|
label={t('journey.studio.layers.distributeH')}
|
|
icon={AlignHorizontalDistributeCenter}
|
|
disabled={units < 3}
|
|
onClick={() => distribute(spreadIndex, 'horizontal')}
|
|
/>
|
|
<IconButton
|
|
label={t('journey.studio.layers.distributeV')}
|
|
icon={AlignVerticalDistributeCenter}
|
|
disabled={units < 3}
|
|
onClick={() => distribute(spreadIndex, 'vertical')}
|
|
/>
|
|
</span>
|
|
</div>
|
|
</Line>
|
|
<Line label={t('journey.studio.layers.alignTo')} wide>
|
|
<Choice<BookAlignTo>
|
|
value={to}
|
|
ariaLabel={t('journey.studio.layers.alignTo')}
|
|
options={[
|
|
{ value: 'selection' as const, label: t('journey.studio.layers.toSelection'), disabled: units < 2 },
|
|
{ value: 'page' as const, label: t('journey.studio.layers.toPage') },
|
|
{ value: 'safe' as const, label: t('journey.studio.layers.toSafe') },
|
|
]}
|
|
onPick={setChosenTo}
|
|
/>
|
|
</Line>
|
|
<Line label={t('journey.studio.order')} wide>
|
|
<div className="st-row st-arrange-row">
|
|
<span className="st-arrange-set">
|
|
{STACK.map((s) => (
|
|
<IconButton
|
|
key={s.to}
|
|
label={t(s.labelKey)}
|
|
icon={s.icon}
|
|
onClick={() => raise(spreadIndex, selection, s.to)}
|
|
/>
|
|
))}
|
|
</span>
|
|
<span className="st-arrange-set">
|
|
{(canGroup || !grouped) && (
|
|
<IconButton
|
|
label={t('journey.studio.layers.groupAction')}
|
|
icon={Group}
|
|
disabled={!canGroup}
|
|
shortcut={keys('G')}
|
|
onClick={() => group(spreadIndex)}
|
|
/>
|
|
)}
|
|
{grouped && (
|
|
<IconButton
|
|
label={t('journey.studio.layers.ungroup')}
|
|
icon={Ungroup}
|
|
shortcut={keys('⇧', 'G')}
|
|
onClick={() => ungroup(spreadIndex)}
|
|
/>
|
|
)}
|
|
{onShowLayers && (
|
|
<IconButton label={t('journey.studio.showLayers')} icon={Layers} onClick={onShowLayers} />
|
|
)}
|
|
</span>
|
|
</div>
|
|
</Line>
|
|
<Line label={t('journey.studio.style')} wide>
|
|
<div className="st-row">
|
|
<IconButton
|
|
label={t('journey.studio.layers.copyStyle')}
|
|
icon={Pipette}
|
|
shortcut={keys('alt', 'C')}
|
|
onClick={() => copyStyle(spreadIndex)}
|
|
/>
|
|
<IconButton
|
|
label={t('journey.studio.layers.pasteStyle')}
|
|
icon={Paintbrush}
|
|
disabled={!hasStyle || !movable.length}
|
|
shortcut={keys('alt', 'V')}
|
|
onClick={() => pasteStyle(spreadIndex)}
|
|
/>
|
|
</div>
|
|
</Line>
|
|
</div>
|
|
{asGroup && <p className="st-hint st-arrange-hint">{t('journey.studio.layers.groupHint')}</p>}
|
|
</Section>
|
|
);
|
|
}
|