mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-06 13:35:05 +00:00
238 lines
7.3 KiB
TypeScript
238 lines
7.3 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Button,
|
|
ColorInput,
|
|
Group,
|
|
NumberInput,
|
|
Paper,
|
|
SegmentedControl,
|
|
Select,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
Tooltip,
|
|
} from '@mantine/core';
|
|
import { IconBold, IconItalic, IconTrash } from '@tabler/icons-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import type { TemplateFieldPlacement, TemplateVariable } from '@ema-platform/api';
|
|
|
|
interface Props {
|
|
block: TemplateFieldPlacement | null;
|
|
variables: TemplateVariable[];
|
|
onChange: (block: TemplateFieldPlacement) => void;
|
|
onDelete: (id: string) => void;
|
|
disabled: boolean;
|
|
}
|
|
|
|
/** Everything about the selected block that is not its position on the page. */
|
|
export function BlockPropertiesPanel({
|
|
block,
|
|
variables,
|
|
onChange,
|
|
onDelete,
|
|
disabled,
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
|
|
if (!block) {
|
|
return (
|
|
<Paper withBorder p="md" radius="md">
|
|
<Text size="sm" c="dimmed">
|
|
{t('designer.noBlockSelected', 'Select a block on the page to edit it.')}
|
|
</Text>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
const current = block;
|
|
const isLiteral = current.variable === null;
|
|
const isImage = current.type === 'image';
|
|
const variableByKey = new Map(variables.map((v) => [v.key, v]));
|
|
|
|
const selectVariable = (key: string | null) => {
|
|
if (!key) return;
|
|
const kind: 'text' | 'image' =
|
|
variableByKey.get(key)?.kind === 'image' ? 'image' : 'text';
|
|
onChange({ ...current, variable: key, type: kind });
|
|
};
|
|
|
|
return (
|
|
<Paper withBorder p="md" radius="md">
|
|
<Stack gap="sm">
|
|
<Group justify="space-between" align="center">
|
|
<Text fw={600} size="sm">
|
|
{t('designer.blockProperties', 'Selected block')}
|
|
</Text>
|
|
<Tooltip label={t('designer.deleteBlock', 'Remove block')}>
|
|
<ActionIcon
|
|
variant="subtle"
|
|
color="red"
|
|
disabled={disabled}
|
|
onClick={() => onDelete(block.id)}
|
|
>
|
|
<IconTrash size={16} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
|
|
<SegmentedControl
|
|
fullWidth
|
|
size="xs"
|
|
value={isLiteral ? 'text' : 'variable'}
|
|
disabled={disabled}
|
|
onChange={(value) =>
|
|
value === 'text'
|
|
? onChange({ ...block, variable: null, type: 'text', text: block.text ?? '' })
|
|
: selectVariable(variables[0]?.key ?? 'companyName')
|
|
}
|
|
data={[
|
|
{ value: 'variable', label: t('designer.blockVariable', 'Variable') },
|
|
{ value: 'text', label: t('designer.blockText', 'Fixed text') },
|
|
]}
|
|
/>
|
|
|
|
{isLiteral ? (
|
|
<TextInput
|
|
label={t('designer.blockTextLabel', 'Text')}
|
|
value={block.text ?? ''}
|
|
onChange={(e) => onChange({ ...block, text: e.currentTarget.value })}
|
|
disabled={disabled}
|
|
/>
|
|
) : (
|
|
<Select
|
|
label={t('designer.blockVariableLabel', 'Variable')}
|
|
data={variables.map((variable) => ({
|
|
value: variable.key,
|
|
label: variable.kind === 'image' ? `🖼 ${variable.label}` : variable.label,
|
|
}))}
|
|
value={block.variable}
|
|
onChange={selectVariable}
|
|
searchable
|
|
disabled={disabled}
|
|
/>
|
|
)}
|
|
|
|
<Group gap="xs" grow>
|
|
{!isImage && (
|
|
<NumberInput
|
|
label={t('designer.blockFontSize', 'Font size')}
|
|
value={block.fontSize ?? 14}
|
|
onChange={(value) => onChange({ ...block, fontSize: Number(value) || 14 })}
|
|
min={4}
|
|
max={200}
|
|
disabled={disabled}
|
|
/>
|
|
)}
|
|
<NumberInput
|
|
label={t('designer.blockWidth', 'Width (%)')}
|
|
value={block.widthPct}
|
|
onChange={(value) =>
|
|
onChange({ ...block, widthPct: Math.min(100, Math.max(1, Number(value) || 1)) })
|
|
}
|
|
min={1}
|
|
max={100}
|
|
disabled={disabled}
|
|
/>
|
|
</Group>
|
|
|
|
<NumberInput
|
|
label={t('designer.blockPage', 'Page')}
|
|
description={t('designer.blockPageHint', 'Which sheet of the document this block prints on')}
|
|
value={(block.page ?? 0) + 1}
|
|
onChange={(value) => {
|
|
const index = Math.max(0, Math.min(63, Math.floor(Number(value) || 1) - 1));
|
|
// Omitted on the first page so a single-page design stays as it was.
|
|
const { page: _page, ...rest } = block;
|
|
onChange(index > 0 ? { ...rest, page: index } : rest);
|
|
}}
|
|
min={1}
|
|
max={64}
|
|
disabled={disabled}
|
|
/>
|
|
|
|
<Group gap="xs" grow>
|
|
<NumberInput
|
|
label={t('designer.blockX', 'X (%)')}
|
|
value={block.xPct}
|
|
onChange={(value) =>
|
|
onChange({ ...block, xPct: Math.min(100, Math.max(0, Number(value) || 0)) })
|
|
}
|
|
min={0}
|
|
max={100}
|
|
decimalScale={2}
|
|
disabled={disabled}
|
|
/>
|
|
<NumberInput
|
|
label={t('designer.blockY', 'Y (%)')}
|
|
value={block.yPct}
|
|
onChange={(value) =>
|
|
onChange({ ...block, yPct: Math.min(100, Math.max(0, Number(value) || 0)) })
|
|
}
|
|
min={0}
|
|
max={100}
|
|
decimalScale={2}
|
|
disabled={disabled}
|
|
/>
|
|
</Group>
|
|
|
|
{!isImage && (
|
|
<>
|
|
<SegmentedControl
|
|
fullWidth
|
|
size="xs"
|
|
value={block.align ?? 'left'}
|
|
disabled={disabled}
|
|
onChange={(value) =>
|
|
onChange({ ...block, align: value as TemplateFieldPlacement['align'] })
|
|
}
|
|
data={[
|
|
{ value: 'left', label: t('designer.alignLeft', 'Left') },
|
|
{ value: 'center', label: t('designer.alignCenter', 'Centre') },
|
|
{ value: 'right', label: t('designer.alignRight', 'Right') },
|
|
{ value: 'justify', label: t('designer.alignJustify', 'Justify') },
|
|
]}
|
|
/>
|
|
|
|
<Group gap="xs">
|
|
<Button
|
|
size="xs"
|
|
variant={block.fontWeight === 'bold' ? 'filled' : 'default'}
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
onChange({
|
|
...block,
|
|
fontWeight: block.fontWeight === 'bold' ? 'normal' : 'bold',
|
|
})
|
|
}
|
|
>
|
|
<IconBold size={14} />
|
|
</Button>
|
|
<Button
|
|
size="xs"
|
|
variant={block.fontStyle === 'italic' ? 'filled' : 'default'}
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
onChange({
|
|
...block,
|
|
fontStyle: block.fontStyle === 'italic' ? 'normal' : 'italic',
|
|
})
|
|
}
|
|
>
|
|
<IconItalic size={14} />
|
|
</Button>
|
|
</Group>
|
|
|
|
<ColorInput
|
|
label={t('designer.blockColor', 'Colour')}
|
|
value={block.color ?? '#111111'}
|
|
onChange={(value) => onChange({ ...block, color: value })}
|
|
disabled={disabled}
|
|
format="hex"
|
|
/>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|