feat: iframe-based PagedJS preview with reactive CSS editor

- Isolate PagedJS in iframe to avoid DOM/CSS conflicts
- Add EditorPanel for global CSS controls
- Add StylesheetViewer with highlight.js syntax highlighting
- Add ElementPopup for element-specific CSS editing
- CSS modifications update preview reactively
- Support px/rem/em units

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
isUnknown 2025-11-24 16:51:55 +01:00
parent dc0ae26464
commit f51c77cefe
9 changed files with 541 additions and 109 deletions

View file

@ -0,0 +1,72 @@
<template>
<aside id="editor-panel">
<h3>Éditeur</h3>
<div class="control">
<label>Font-size .about</label>
<input
type="number"
step="0.1"
v-model.number="localFontSize"
@input="updateFontSize"
/>
<span>{{ unit }}</span>
</div>
</aside>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
fontSize: Number,
unit: String,
});
const emit = defineEmits(['update:fontSize']);
const localFontSize = ref(props.fontSize);
watch(
() => props.fontSize,
(newVal) => {
localFontSize.value = newVal;
}
);
const updateFontSize = () => {
emit('update:fontSize', localFontSize.value);
};
</script>
<style scoped>
#editor-panel {
position: fixed;
top: 0;
left: 0;
width: 250px;
height: 100vh;
background: #f5f5f5;
padding: 1rem;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
overflow-y: auto;
}
h3 {
margin-top: 0;
}
.control {
margin-bottom: 1rem;
}
.control label {
display: block;
margin-bottom: 0.25rem;
font-size: 0.875rem;
}
.control input {
width: 80px;
padding: 0.25rem;
}
</style>

View file

@ -0,0 +1,152 @@
<template>
<div
v-if="visible"
id="element-popup"
:style="{ top: position.y + 'px', left: position.x + 'px' }"
>
<div class="popup-header">
<span>{{ selector }}</span>
<button class="close-btn" @click="$emit('close')">×</button>
</div>
<div class="popup-body">
<div class="popup-controls">
<div class="control" v-if="currentFontSize !== null">
<label>font-size</label>
<input
type="number"
step="0.1"
:value="currentFontSize"
@input="$emit('update:fontSize', parseFloat($event.target.value))"
/>
<span>{{ fontSizeUnit }}</span>
</div>
<p v-else class="no-styles">Aucun style éditable</p>
</div>
<div class="popup-css">
<pre><code class="hljs language-css" v-html="highlightedCss"></code></pre>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import hljs from 'highlight.js/lib/core';
import css from 'highlight.js/lib/languages/css';
import 'highlight.js/styles/github.css';
hljs.registerLanguage('css', css);
const props = defineProps({
visible: Boolean,
position: Object,
selector: String,
elementCss: String,
currentFontSize: Number,
fontSizeUnit: String
});
defineEmits(['close', 'update:fontSize']);
const highlightedCss = computed(() => {
if (!props.elementCss) return '<span class="no-css">Aucun style défini</span>';
return hljs.highlight(props.elementCss, { language: 'css' }).value;
});
</script>
<style scoped>
#element-popup {
position: fixed;
background: white;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
min-width: 400px;
max-width: 500px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid #eee;
font-size: 0.875rem;
font-weight: bold;
background: #f5f5f5;
}
.close-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1.25rem;
line-height: 1;
padding: 0;
}
.popup-body {
display: flex;
gap: 1px;
background: #eee;
}
.popup-controls {
flex: 1;
padding: 0.75rem;
background: white;
}
.popup-css {
flex: 1;
padding: 0.75rem;
background: #1e1e1e;
max-height: 200px;
overflow-y: auto;
}
.popup-css pre {
margin: 0;
font-size: 0.75rem;
line-height: 1.4;
}
.popup-css code {
background: transparent;
color: #fff;
}
.control {
margin-bottom: 0.5rem;
}
.control label {
display: block;
font-size: 0.75rem;
margin-bottom: 0.25rem;
color: #666;
}
.control input {
width: 60px;
padding: 0.25rem;
font-size: 0.875rem;
}
.control span {
font-size: 0.75rem;
color: #666;
margin-left: 0.25rem;
}
.no-styles {
font-size: 0.75rem;
color: #999;
margin: 0;
}
.no-css {
color: #666;
font-style: italic;
}
</style>

View file

@ -1,6 +1,33 @@
<template>
<section class="chapter">
<h2>About</h2>
<p class="about">
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex
ipsum erat lacus arcu nunc cursus a scelerisque tristique ipsum congue
adipiscing suspendisse facilisis dolor morbi nulla orci massa. Vivamus nec
nisl amet eros consectetur ut consectetur phasellus maecenas morbi felis
pellentesque pellentesque ipsum ut a arcu sem facilisis eros tempus eu
euismod sollicitudin. Nisl facilisis tempus tempus placerat lorem sed leo
sit a leo tempus amet tristique felis gravida morbi congue aliquam nunc
maximus ipsum ex nisl a. Leo felis leo gravida fusce lacus orci
condimentum morbi eros amet portaest sit quam a hendrerit fusce quam
tristique arcu id maximus nunc fusce suspendisse.
</p>
<p class="about">
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit phasellus
euismod maximus rutrum vivamus dolor erat sollicitudin ut quam metus
gravida proin nisl lacus sed lacus. Morbi fusce eros euismod varius ex
ipsum erat lacus arcu nunc cursus a scelerisque tristique ipsum congue
adipiscing suspendisse facilisis dolor morbi nulla orci massa. Vivamus nec
nisl amet eros consectetur ut consectetur phasellus maecenas morbi felis
pellentesque pellentesque ipsum ut a arcu sem facilisis eros tempus eu
euismod sollicitudin. Nisl facilisis tempus tempus placerat lorem sed leo
sit a leo tempus amet tristique felis gravida morbi congue aliquam nunc
maximus ipsum ex nisl a. Leo felis leo gravida fusce lacus orci
condimentum morbi eros amet portaest sit quam a hendrerit fusce quam
tristique arcu id maximus nunc fusce suspendisse.
</p>
<p>
Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor
</p>
@ -12,7 +39,7 @@
</section>
<section class="chapter">
<h2>Chapter 2</h2>
<h2 id="chapter-2">Chapter 2</h2>
<p>consectetur adipiscing elit</p>
</section>
@ -22,8 +49,6 @@
</section>
</template>
<script setup>
</script>
<script setup></script>
<style>
</style>
<style></style>

View file

@ -0,0 +1,22 @@
<template>
<aside id="side-panel">
<button @click="fontSize++">Action ({{ fontSize }}px)</button>
</aside>
</template>
<script setup>
const fontSize = defineModel('fontSize');
</script>
<style>
#side-panel {
position: fixed;
top: 0;
right: 0;
width: 300px;
height: 100vh;
background: white;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
padding: 1rem;
}
</style>

View file

@ -0,0 +1,54 @@
<template>
<aside id="stylesheet-viewer">
<h3>Stylesheet</h3>
<pre><code class="hljs language-css" v-html="highlightedCss"></code></pre>
</aside>
</template>
<script setup>
import { computed } from 'vue';
import hljs from 'highlight.js/lib/core';
import css from 'highlight.js/lib/languages/css';
import 'highlight.js/styles/github.css';
hljs.registerLanguage('css', css);
const props = defineProps({
stylesheet: String
});
const highlightedCss = computed(() => {
if (!props.stylesheet) return '';
return hljs.highlight(props.stylesheet, { language: 'css' }).value;
});
</script>
<style scoped>
#stylesheet-viewer {
position: fixed;
top: 0;
right: 0;
width: 350px;
height: 100vh;
background: #1e1e1e;
padding: 1rem;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
overflow-y: auto;
color: #fff;
}
h3 {
margin-top: 0;
color: #fff;
}
pre {
margin: 0;
font-size: 0.75rem;
line-height: 1.4;
}
code {
background: transparent;
}
</style>