feat: add custom page format with editable width/height fields
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 20s

Keep only A4/A5 + "Personnalisé" option. Width/height fields use same
layout as margin fields and are editable only in custom mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-02-24 14:51:00 +01:00
parent b808e22274
commit 9f62d3ae5d

View file

@ -1,231 +1,258 @@
<template>
<section class="settings-section" id="settings-section_page" data-color-type="page">
<section
class="settings-section"
id="settings-section_page"
data-color-type="page"
>
<h2>Réglage des pages</h2>
<div class="container">
<div class="settings-subsection">
<div class="field field-simple">
<label for="page-format" class="label-with-tooltip" data-css="size"
>Format d'impression</label
>
<select id="page-format" v-model="pageFormat">
<option value="A4">A4</option>
<option value="A5">A5</option>
<option value="A3">A3</option>
<div class="container">
<div class="settings-subsection">
<div class="field field-simple">
<label for="page-format" class="label-with-tooltip" data-css="size"
>Format d'impression</label
>
<select id="page-format" v-model="pageFormat">
<option value="A4">A4</option>
<option value="A5">A5</option>
<!-- <option value="A3">A3</option>
<option value="letter">Letter</option>
<option value="legal">Legal</option>
</select>
</div>
</div>
<div class="settings-subsection">
<div class="field field-size field--view-only">
<label for="page-width" class="label-with-tooltip" data-css="width"
>Largeur</label
>
<input
id="page-width"
type="number"
:value="parseInt(pageWidth)"
disabled
/>
<button type="button" disabled>mm</button>
<option value="legal">Legal</option> -->
<option value="custom">Personnalisé</option>
</select>
</div>
</div>
<div class="field field-size field--view-only">
<label for="page-height" class="label-with-tooltip" data-css="height"
>Hauteur</label
<div class="settings-subsection">
<div
class="field field-margin"
:class="{ 'field--view-only': !isCustomFormat }"
>
<input
id="page-height"
type="number"
:value="parseInt(pageHeight)"
disabled
/>
<button type="button" disabled>mm</button>
<label for="page-width" class="label-with-tooltip" data-css="width"
>Largeur</label
>
<div class="input-with-unit">
<NumberInput
id="page-width"
:modelValue="customWidth"
@update:modelValue="(v) => (customWidth = v)"
:min="50"
:step="1"
:disabled="!isCustomFormat"
/>
<div class="unit-toggle">
<button type="button" class="active">mm</button>
</div>
</div>
</div>
<div
class="field field-margin"
:class="{ 'field--view-only': !isCustomFormat }"
>
<label for="page-height" class="label-with-tooltip" data-css="height"
>Hauteur</label
>
<div class="input-with-unit">
<NumberInput
id="page-height"
:modelValue="customHeight"
@update:modelValue="(v) => (customHeight = v)"
:min="50"
:step="1"
:disabled="!isCustomFormat"
/>
<div class="unit-toggle">
<button type="button" class="active">mm</button>
</div>
</div>
</div>
</div>
</div>
<div class="settings-subsection margins">
<h3>Marges</h3>
<div class="settings-subsection margins">
<h3>Marges</h3>
<div class="field field-margin">
<label for="margin-top" class="label-with-tooltip" data-css="margin-top"
>Haut</label
>
<div class="input-with-unit">
<NumberInput
id="margin-top"
:modelValue="margins.top.value"
:min="0"
:step="1"
@update:modelValue="(value) => margins.top.value = value"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.top.unit === 'mm' }"
@click="updateMarginUnit('top', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.top.unit === 'px' }"
@click="updateMarginUnit('top', 'px')"
>
px
</button>
<!-- <button
<div class="field field-margin">
<label
for="margin-top"
class="label-with-tooltip"
data-css="margin-top"
>Haut</label
>
<div class="input-with-unit">
<NumberInput
id="margin-top"
:modelValue="margins.top.value"
:min="0"
:step="1"
@update:modelValue="(value) => (margins.top.value = value)"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.top.unit === 'mm' }"
@click="updateMarginUnit('top', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.top.unit === 'px' }"
@click="updateMarginUnit('top', 'px')"
>
px
</button>
<!-- <button
type="button"
:class="{ active: margins.top.unit === 'rem' }"
@click="updateMarginUnit('top', 'rem')"
>
rem
</button> -->
</div>
</div>
</div>
</div>
<div class="field field-margin">
<label
for="margin-bottom"
class="label-with-tooltip"
data-css="margin-bottom"
>Bas</label
>
<div class="input-with-unit">
<NumberInput
id="margin-bottom"
:modelValue="margins.bottom.value"
:min="0"
:step="1"
@update:modelValue="(value) => margins.bottom.value = value"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.bottom.unit === 'mm' }"
@click="updateMarginUnit('bottom', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.bottom.unit === 'px' }"
@click="updateMarginUnit('bottom', 'px')"
>
px
</button>
<!-- <button
<div class="field field-margin">
<label
for="margin-bottom"
class="label-with-tooltip"
data-css="margin-bottom"
>Bas</label
>
<div class="input-with-unit">
<NumberInput
id="margin-bottom"
:modelValue="margins.bottom.value"
:min="0"
:step="1"
@update:modelValue="(value) => (margins.bottom.value = value)"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.bottom.unit === 'mm' }"
@click="updateMarginUnit('bottom', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.bottom.unit === 'px' }"
@click="updateMarginUnit('bottom', 'px')"
>
px
</button>
<!-- <button
type="button"
:class="{ active: margins.bottom.unit === 'rem' }"
@click="updateMarginUnit('bottom', 'rem')"
>
rem
</button> -->
</div>
</div>
</div>
</div>
<div class="field field-margin">
<label
for="margin-left"
class="label-with-tooltip"
data-css="margin-left"
>Gauche</label
>
<div class="input-with-unit">
<NumberInput
id="margin-left"
:modelValue="margins.left.value"
:min="0"
:step="1"
@update:modelValue="(value) => margins.left.value = value"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.left.unit === 'mm' }"
@click="updateMarginUnit('left', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.left.unit === 'px' }"
@click="updateMarginUnit('left', 'px')"
>
px
</button>
<!-- <button
<div class="field field-margin">
<label
for="margin-left"
class="label-with-tooltip"
data-css="margin-left"
>Gauche</label
>
<div class="input-with-unit">
<NumberInput
id="margin-left"
:modelValue="margins.left.value"
:min="0"
:step="1"
@update:modelValue="(value) => (margins.left.value = value)"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.left.unit === 'mm' }"
@click="updateMarginUnit('left', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.left.unit === 'px' }"
@click="updateMarginUnit('left', 'px')"
>
px
</button>
<!-- <button
type="button"
:class="{ active: margins.left.unit === 'rem' }"
@click="updateMarginUnit('left', 'rem')"
>
rem
</button> -->
</div>
</div>
</div>
</div>
<div class="field field-margin">
<label
for="margin-right"
class="label-with-tooltip"
data-css="margin-right"
>Droite</label
>
<div class="input-with-unit">
<NumberInput
id="margin-right"
:modelValue="margins.right.value"
:min="0"
:step="1"
@update:modelValue="(value) => margins.right.value = value"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.right.unit === 'mm' }"
@click="updateMarginUnit('right', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.right.unit === 'px' }"
@click="updateMarginUnit('right', 'px')"
>
px
</button>
<!-- <button
<div class="field field-margin">
<label
for="margin-right"
class="label-with-tooltip"
data-css="margin-right"
>Droite</label
>
<div class="input-with-unit">
<NumberInput
id="margin-right"
:modelValue="margins.right.value"
:min="0"
:step="1"
@update:modelValue="(value) => (margins.right.value = value)"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: margins.right.unit === 'mm' }"
@click="updateMarginUnit('right', 'mm')"
>
mm
</button>
<button
type="button"
:class="{ active: margins.right.unit === 'px' }"
@click="updateMarginUnit('right', 'px')"
>
px
</button>
<!-- <button
type="button"
:class="{ active: margins.right.unit === 'rem' }"
@click="updateMarginUnit('right', 'rem')"
>
rem
</button> -->
</div>
</div>
</div>
</div>
</div>
<div class="settings-subsection">
<div class="field field-simple">
<label for="background" class="label-with-tooltip" data-css="background"
>Arrière-plan</label
>
<div class="input-with-color">
<input
ref="backgroundColorInput"
type="text"
id="background"
v-model="background.value"
data-coloris
/>
<!-- Temporarily commented out
<div class="settings-subsection">
<div class="field field-simple">
<label
for="background"
class="label-with-tooltip"
data-css="background"
>Arrière-plan</label
>
<div class="input-with-color">
<input
ref="backgroundColorInput"
type="text"
id="background"
v-model="background.value"
data-coloris
/>
<!-- Temporarily commented out
<div class="unit-toggle">
<button
type="button"
@ -242,50 +269,49 @@
hex
</button>
</div>
-->
--></div>
</div>
</div>
<div class="settings-subsection">
<div class="field field-simple">
<label
for="pattern"
class="label-with-tooltip"
data-css="background-image"
>Motif</label
>
<select id="pattern" v-model="pattern">
<option value="">Choisissez</option>
<option value="dots">Points</option>
<option value="lines">Lignes</option>
<option value="grid">Grille</option>
</select>
</div>
</div>
<div class="settings-subsection">
<div class="field checkbox-field">
<input id="page-numbers" type="checkbox" v-model="pageNumbers" />
<label
for="page-numbers"
class="label-with-tooltip"
data-css="@bottom-left/right"
>Numéro de page</label
>
</div>
<div class="field checkbox-field">
<input id="running-title" type="checkbox" v-model="runningTitle" />
<label
for="running-title"
class="label-with-tooltip"
data-css="string-set"
>Titre courant</label
>
</div>
</div>
</div>
<div class="settings-subsection">
<div class="field field-simple">
<label
for="pattern"
class="label-with-tooltip"
data-css="background-image"
>Motif</label
>
<select id="pattern" v-model="pattern">
<option value="">Choisissez</option>
<option value="dots">Points</option>
<option value="lines">Lignes</option>
<option value="grid">Grille</option>
</select>
</div>
</div>
<div class="settings-subsection">
<div class="field checkbox-field">
<input id="page-numbers" type="checkbox" v-model="pageNumbers" />
<label
for="page-numbers"
class="label-with-tooltip"
data-css="@bottom-left/right"
>Numéro de page</label
>
</div>
<div class="field checkbox-field">
<input id="running-title" type="checkbox" v-model="runningTitle" />
<label
for="running-title"
class="label-with-tooltip"
data-css="string-set"
>Titre courant</label
>
</div>
</div>
</div>
</section>
</template>
@ -308,15 +334,16 @@ let isUpdatingFromStore = false;
const pageFormat = ref('A4');
const pageFormats = {
A4: { width: '210mm', height: '297mm' },
A5: { width: '148mm', height: '210mm' },
A3: { width: '297mm', height: '420mm' },
letter: { width: '8.5in', height: '11in' },
legal: { width: '8.5in', height: '14in' },
A4: { width: 210, height: 297 },
A5: { width: 148, height: 210 },
// A3: { width: 297, height: 420 },
// letter: { width: 216, height: 279 },
// legal: { width: 216, height: 356 },
};
const pageWidth = computed(() => pageFormats[pageFormat.value].width);
const pageHeight = computed(() => pageFormats[pageFormat.value].height);
const customWidth = ref(210);
const customHeight = ref(297);
const isCustomFormat = computed(() => pageFormat.value === 'custom');
const margins = ref({
top: { value: 20, unit: 'mm' },
@ -347,8 +374,36 @@ const immediateUpdate = (callback) => {
watch(pageFormat, (newFormat) => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
stylesheetStore.updateProperty('@page', 'size', newFormat, '');
if (newFormat === 'custom') {
immediateUpdate(() => {
stylesheetStore.updateProperty(
'@page',
'size',
`${customWidth.value}mm ${customHeight.value}mm`,
''
);
});
} else {
const format = pageFormats[newFormat];
if (format) {
customWidth.value = format.width;
customHeight.value = format.height;
}
immediateUpdate(() => {
stylesheetStore.updateProperty('@page', 'size', newFormat, '');
});
}
});
watch([customWidth, customHeight], () => {
if (isUpdatingFromStore || !isCustomFormat.value) return;
debouncedUpdate(() => {
stylesheetStore.updateProperty(
'@page',
'size',
`${customWidth.value}mm ${customHeight.value}mm`,
''
);
});
});
@ -546,9 +601,24 @@ const syncFromStore = () => {
try {
const pageBlock = stylesheetStore.extractBlock('@page');
const sizeMatch = pageBlock.match(/size:\s*([A-Za-z0-9]+)/);
if (sizeMatch) {
pageFormat.value = sizeMatch[1];
// Try named format first (A4, A5), then custom dimensions
const namedSizeMatch = pageBlock.match(/size:\s*(A4|A5)\b/);
if (namedSizeMatch) {
pageFormat.value = namedSizeMatch[1];
const format = pageFormats[namedSizeMatch[1]];
if (format) {
customWidth.value = format.width;
customHeight.value = format.height;
}
} else {
const customSizeMatch = pageBlock.match(
/size:\s*([0-9.]+)mm\s+([0-9.]+)mm/
);
if (customSizeMatch) {
customWidth.value = parseFloat(customSizeMatch[1]);
customHeight.value = parseFloat(customSizeMatch[2]);
pageFormat.value = 'custom';
}
}
const marginMatch = pageBlock.match(