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