feat: implement reactive EditorPanel with bidirectional sync
- Reorganize editor components into dedicated folder - Create PageSettings component with page format, margins, background controls - Create TextSettings component (structure only, to be populated) - Implement debounced updates (1s delay) to stylesheet store - Add bidirectional sync between EditorPanel and StylesheetViewer - Preserve scroll position as percentage when reloading preview - Move @page rules from App.vue to stylesheet.css for unified management - Extend css-parsing utils to handle text values (e.g., 'A4', 'portrait') - Remove unnecessary comments, use explicit naming instead 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b8cb77c0e5
commit
9f10971041
7 changed files with 1104 additions and 166 deletions
360
src/components/editor/PageSettings.vue
Normal file
360
src/components/editor/PageSettings.vue
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
<template>
|
||||
<section class="settings-section">
|
||||
<h2>Réglage des pages</h2>
|
||||
|
||||
<div class="field">
|
||||
<label for="page-format">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 class="field">
|
||||
<label for="page-width">Largeur</label>
|
||||
<input
|
||||
id="page-width"
|
||||
type="text"
|
||||
:value="pageWidth"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="page-height">Hauteur</label>
|
||||
<input
|
||||
id="page-height"
|
||||
type="text"
|
||||
:value="pageHeight"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<h3>Marges</h3>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-top">Haut</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-top"
|
||||
type="number"
|
||||
v-model.number="margins.top.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.top.unit === 'mm' }"
|
||||
@click="margins.top.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.top.unit === 'px' }"
|
||||
@click="margins.top.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-bottom">Bas</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-bottom"
|
||||
type="number"
|
||||
v-model.number="margins.bottom.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.bottom.unit === 'mm' }"
|
||||
@click="margins.bottom.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.bottom.unit === 'px' }"
|
||||
@click="margins.bottom.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-left">Gauche</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-left"
|
||||
type="number"
|
||||
v-model.number="margins.left.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.left.unit === 'mm' }"
|
||||
@click="margins.left.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.left.unit === 'px' }"
|
||||
@click="margins.left.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="margin-right">Droite</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="margin-right"
|
||||
type="number"
|
||||
v-model.number="margins.right.value"
|
||||
min="0"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.right.unit === 'mm' }"
|
||||
@click="margins.right.unit = 'mm'"
|
||||
>
|
||||
mm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: margins.right.unit === 'px' }"
|
||||
@click="margins.right.unit = 'px'"
|
||||
>
|
||||
px
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="background">Arrière-plan</label>
|
||||
<div class="field-with-unit">
|
||||
<input
|
||||
id="background"
|
||||
type="text"
|
||||
v-model="background.value"
|
||||
/>
|
||||
<div class="unit-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: background.format === 'rgb' }"
|
||||
@click="background.format = 'rgb'"
|
||||
>
|
||||
rgb
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: background.format === 'hex' }"
|
||||
@click="background.format = 'hex'"
|
||||
>
|
||||
hex
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="pattern">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 class="field checkbox-field">
|
||||
<input
|
||||
id="page-numbers"
|
||||
type="checkbox"
|
||||
v-model="pageNumbers"
|
||||
/>
|
||||
<label for="page-numbers">Numéro de page</label>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox-field">
|
||||
<input
|
||||
id="running-title"
|
||||
type="checkbox"
|
||||
v-model="runningTitle"
|
||||
/>
|
||||
<label for="running-title">Titre courant</label>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useStylesheetStore } from '../../stores/stylesheet';
|
||||
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
|
||||
let isUpdatingFromStore = false;
|
||||
let updateTimer = null;
|
||||
|
||||
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' }
|
||||
};
|
||||
|
||||
const pageWidth = computed(() => pageFormats[pageFormat.value].width);
|
||||
const pageHeight = computed(() => pageFormats[pageFormat.value].height);
|
||||
|
||||
const margins = ref({
|
||||
top: { value: 20, unit: 'mm' },
|
||||
bottom: { value: 20, unit: 'mm' },
|
||||
left: { value: 20, unit: 'mm' },
|
||||
right: { value: 20, unit: 'mm' }
|
||||
});
|
||||
|
||||
const background = ref({
|
||||
value: '',
|
||||
format: 'hex'
|
||||
});
|
||||
|
||||
const pattern = ref('');
|
||||
const pageNumbers = ref(false);
|
||||
const runningTitle = ref(false);
|
||||
|
||||
const debouncedUpdate = (callback) => {
|
||||
clearTimeout(updateTimer);
|
||||
updateTimer = setTimeout(callback, 1000);
|
||||
};
|
||||
|
||||
watch(pageFormat, (newFormat) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
stylesheetStore.updateProperty('@page', 'size', newFormat, '');
|
||||
});
|
||||
});
|
||||
|
||||
watch(margins, (newMargins) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
const marginValue = `${newMargins.top.value}${newMargins.top.unit} ${newMargins.right.value}${newMargins.right.unit} ${newMargins.bottom.value}${newMargins.bottom.unit} ${newMargins.left.value}${newMargins.left.unit}`;
|
||||
|
||||
const currentBlock = stylesheetStore.extractBlock('@page');
|
||||
const updatedBlock = currentBlock.replace(
|
||||
/(margin:\s*)[^;]+/,
|
||||
`$1${marginValue}`
|
||||
);
|
||||
|
||||
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
watch(background, (newBg) => {
|
||||
if (!newBg.value) return;
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
const currentBlock = stylesheetStore.extractBlock('@page');
|
||||
|
||||
if (currentBlock.includes('background:')) {
|
||||
const updatedBlock = currentBlock.replace(
|
||||
/(background:\s*)[^;]+/,
|
||||
`$1${newBg.value}`
|
||||
);
|
||||
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
|
||||
} else {
|
||||
const updatedBlock = currentBlock.replace(
|
||||
/(\s*})$/,
|
||||
` background: ${newBg.value};\n$1`
|
||||
);
|
||||
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
|
||||
}
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
watch(pattern, (newPattern) => {
|
||||
if (!newPattern || isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
// TODO: implement pattern application
|
||||
});
|
||||
});
|
||||
|
||||
watch(pageNumbers, (enabled) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
// TODO: implement page numbers toggle
|
||||
});
|
||||
});
|
||||
|
||||
watch(runningTitle, (enabled) => {
|
||||
if (isUpdatingFromStore) return;
|
||||
|
||||
debouncedUpdate(() => {
|
||||
// TODO: implement running title toggle
|
||||
});
|
||||
});
|
||||
|
||||
const syncFromStore = () => {
|
||||
isUpdatingFromStore = true;
|
||||
|
||||
try {
|
||||
const pageBlock = stylesheetStore.extractBlock('@page');
|
||||
|
||||
const sizeMatch = pageBlock.match(/size:\s*([A-Za-z0-9]+)/);
|
||||
if (sizeMatch) {
|
||||
pageFormat.value = sizeMatch[1];
|
||||
}
|
||||
|
||||
const marginMatch = pageBlock.match(/margin:\s*([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)/i);
|
||||
if (marginMatch) {
|
||||
margins.value.top = { value: parseFloat(marginMatch[1]), unit: marginMatch[2] };
|
||||
margins.value.right = { value: parseFloat(marginMatch[3]), unit: marginMatch[4] };
|
||||
margins.value.bottom = { value: parseFloat(marginMatch[5]), unit: marginMatch[6] };
|
||||
margins.value.left = { value: parseFloat(marginMatch[7]), unit: marginMatch[8] };
|
||||
}
|
||||
|
||||
const bgMatch = pageBlock.match(/background:\s*([^;]+)/);
|
||||
if (bgMatch) {
|
||||
background.value.value = bgMatch[1].trim();
|
||||
}
|
||||
} finally {
|
||||
isUpdatingFromStore = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => stylesheetStore.content, () => {
|
||||
if (!isUpdatingFromStore) {
|
||||
syncFromStore();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
syncFromStore();
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue