Badges : ordre de superposition des champs
La photo recouvrait le nom et le texte libre (champs dessinés dans l'ordre alphabétique de leur type). BadgeField.z_order (migration 0028, champs existants renumérotés photo dessous) fixe l'ordre de dessin du PDF et de l'aperçu ; boutons « Premier plan / Avancer / Reculer / Arrière-plan » dans l'inspecteur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4f86398ccb
commit
1026d9b9e1
@@ -20,6 +20,7 @@ import {
|
||||
isVerticallyCentered,
|
||||
renderField,
|
||||
SAFE_MARGIN_MM,
|
||||
stackOrder,
|
||||
type BadgeData,
|
||||
type FieldState,
|
||||
} from './model';
|
||||
@@ -84,7 +85,8 @@ export function BadgeCanvas({
|
||||
onDropImage,
|
||||
}: BadgeCanvasProps) {
|
||||
const [dropping, setDropping] = useState(false);
|
||||
const shown = fields.filter((f) => f.enabled);
|
||||
// Du dessous vers le dessus, comme le PDF (`z_order`).
|
||||
const shown = stackOrder(fields, face);
|
||||
const isFaceSelected = shown.some((f) => f.field_type === selectedType);
|
||||
|
||||
return (
|
||||
@@ -148,10 +150,11 @@ export function BadgeCanvas({
|
||||
style={{ inset: SAFE_MARGIN_MM * scale }}
|
||||
/>
|
||||
) : null}
|
||||
{shown.map((field) => (
|
||||
{shown.map((field, i) => (
|
||||
<FieldBox
|
||||
key={field.field_type}
|
||||
field={field}
|
||||
layer={i + 1}
|
||||
data={data}
|
||||
volunteer={volunteer}
|
||||
photoUrl={photoUrl}
|
||||
@@ -171,6 +174,7 @@ export function BadgeCanvas({
|
||||
|
||||
function FieldBox({
|
||||
field,
|
||||
layer,
|
||||
data,
|
||||
volunteer,
|
||||
photoUrl,
|
||||
@@ -183,6 +187,8 @@ function FieldBox({
|
||||
onChange,
|
||||
}: {
|
||||
field: FieldState;
|
||||
/** Rang dans la pile de la face (`z-index`). */
|
||||
layer: number;
|
||||
data: BadgeData;
|
||||
volunteer: VolunteerRead | null;
|
||||
photoUrl: string | null;
|
||||
@@ -263,45 +269,58 @@ function FieldBox({
|
||||
const layout = rendered && rendered.kind !== 'photo' ? rendered.layout : null;
|
||||
const truncated = layout?.truncated ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerDown={start('move')}
|
||||
onPointerMove={move}
|
||||
onPointerUp={end}
|
||||
onPointerCancel={end}
|
||||
className="absolute cursor-move touch-none"
|
||||
style={{
|
||||
left: field.x_mm * scale,
|
||||
top: field.y_mm * scale,
|
||||
width: field.width_mm * scale,
|
||||
height: field.height_mm * scale,
|
||||
outline: selected
|
||||
? '2px solid #2A6BFF'
|
||||
: truncated
|
||||
? '2px dashed #FF5C39'
|
||||
: showGuides
|
||||
? '1px dashed rgba(20,17,14,.45)'
|
||||
: 'none',
|
||||
// Le champ sélectionné passe devant les autres : sinon un champ
|
||||
// recouvert (ex. la photo sous un texte) ne se saisit plus.
|
||||
zIndex: selected ? 10 : undefined,
|
||||
}}
|
||||
>
|
||||
{rendered?.kind === 'photo' || (!volunteer && field.field_type === 'PHOTO') ? (
|
||||
<PhotoContent field={field} volunteer={volunteer} photoUrl={photoUrl} scale={scale} />
|
||||
) : rendered?.kind === 'table' ? (
|
||||
<TableContent field={field} layout={rendered.layout} scale={scale} />
|
||||
) : rendered?.kind === 'text' ? (
|
||||
<TextContent field={field} layout={rendered.layout} scale={scale} />
|
||||
) : null}
|
||||
const geometry = {
|
||||
left: field.x_mm * scale,
|
||||
top: field.y_mm * scale,
|
||||
width: field.width_mm * scale,
|
||||
height: field.height_mm * scale,
|
||||
};
|
||||
const handlers = {
|
||||
onPointerDown: start('move'),
|
||||
onPointerMove: move,
|
||||
onPointerUp: end,
|
||||
onPointerCancel: end,
|
||||
};
|
||||
|
||||
{truncated ? (
|
||||
<span className="pointer-events-none absolute -left-[3px] -top-[19px] whitespace-nowrap border-2 border-ink bg-orange px-1 py-[2px] font-display text-[9px] font-extrabold leading-none">
|
||||
{t('badges.preview.truncated')}
|
||||
</span>
|
||||
) : null}
|
||||
return (
|
||||
<>
|
||||
{/* Contenu à son rang dans la pile, comme sur le badge imprimé. */}
|
||||
<div
|
||||
{...(selected ? {} : handlers)}
|
||||
className={cx('absolute touch-none', selected ? 'pointer-events-none' : 'cursor-move')}
|
||||
style={{
|
||||
...geometry,
|
||||
outline: selected
|
||||
? 'none'
|
||||
: truncated
|
||||
? '2px dashed #FF5C39'
|
||||
: showGuides
|
||||
? '1px dashed rgba(20,17,14,.45)'
|
||||
: 'none',
|
||||
zIndex: layer,
|
||||
}}
|
||||
>
|
||||
{rendered?.kind === 'photo' || (!volunteer && field.field_type === 'PHOTO') ? (
|
||||
<PhotoContent field={field} volunteer={volunteer} photoUrl={photoUrl} scale={scale} />
|
||||
) : rendered?.kind === 'table' ? (
|
||||
<TableContent field={field} layout={rendered.layout} scale={scale} />
|
||||
) : rendered?.kind === 'text' ? (
|
||||
<TextContent field={field} layout={rendered.layout} scale={scale} />
|
||||
) : null}
|
||||
|
||||
{truncated && !selected ? <TruncatedTag /> : null}
|
||||
</div>
|
||||
|
||||
{/* Cadre de sélection par-dessus toute la pile : un champ recouvert
|
||||
(ex. la photo sous un texte) reste saisissable, sans que l'aperçu
|
||||
change l'ordre d'impression. */}
|
||||
{selected ? (
|
||||
<>
|
||||
<div
|
||||
{...handlers}
|
||||
className="absolute z-[15] cursor-move touch-none"
|
||||
style={{ ...geometry, outline: '2px solid #2A6BFF' }}
|
||||
>
|
||||
{truncated ? <TruncatedTag /> : null}
|
||||
<span className="pointer-events-none absolute -bottom-[19px] left-0 z-10 whitespace-nowrap bg-blue-strong px-[5px] py-[2px] font-mono text-[9px] font-semibold leading-tight text-white">
|
||||
{t('badges.preview.dims', { width: fmt(field.width_mm), height: fmt(field.height_mm) })}
|
||||
{layout?.reduced
|
||||
@@ -315,9 +334,17 @@ function FieldBox({
|
||||
onPointerCancel={end}
|
||||
className="absolute -bottom-[7px] -right-[7px] h-[15px] w-[15px] cursor-nwse-resize touch-none border-2 border-ink bg-yellow lg:h-[13px] lg:w-[13px]"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TruncatedTag() {
|
||||
return (
|
||||
<span className="pointer-events-none absolute -left-[3px] -top-[19px] whitespace-nowrap border-2 border-ink bg-orange px-1 py-[2px] font-display text-[9px] font-extrabold leading-none">
|
||||
{t('badges.preview.truncated')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user