Compare commits

...

4 commits

Author SHA1 Message Date
isUnknown
9af36fb422 feat: integrate Coloris color picker for background field
Add Coloris.js library for enhanced color selection in PageSettings with
automatic button state sync across tab changes.

Features:
- Color picker with swatches, alpha support, and format toggle (hex/rgb/hsl)
- Button positioned to the left of input field
- Automatic sync when switching tabs (remembers selected color)
- Close button and click-outside-to-close functionality
- Dark theme with pill UI style

Changes:
- Install @melloware/coloris package
- PageSettings.vue: Integrate Coloris with data-coloris attribute,
  add tab visibility detection via provide/inject, force button update
  when returning to document tab
- EditorPanel.vue: Provide activeTab to child components, increase
  panel width to 30rem
- _forms.scss: Add .input-with-color styles with custom Coloris
  button positioning (absolute positioned to left of input)
- Temporarily comment out rgb/hex format buttons (replaced by Coloris
  format toggle)

Technical details:
- Uses provide/inject pattern to detect tab changes
- Triggers synthetic input events to force Coloris button refresh
- Custom CSS overrides to position swatch button correctly

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:03:29 +01:00
isUnknown
467ae905bd refactor: optimize EditorPanel updates with selective debouncing
Implement immediate vs debounced updates based on input type to improve
UX responsiveness while preventing excessive re-renders.

Update strategy:
- Immediate (0ms): select, buttons, checkboxes, color picker
- Debounced (1s): text inputs, number inputs, range sliders

Changes:
- PageSettings.vue: Split watchers for margin values/units and background
  value/format. Extract update logic into reusable functions.
- TextSettings.vue: Add comprehensive watcher system with selective
  debouncing for all settings (font, size, color, margins, etc.)

This ensures button clicks (unit toggles, format switches) apply instantly
while typed values (numbers, text) batch updates to reduce CSS re-parsing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 14:03:40 +01:00
isUnknown
7ed57d000b refactor: integrate StylesheetViewer into EditorPanel code tab
Move StylesheetViewer from standalone fixed panel to integrated component
within EditorPanel's "code" tab. Maintains full functionality including:
- Bidirectional sync with Pinia store and PagedJS preview
- Toggle between read/edit modes
- CSS syntax highlighting
- Debounced updates

Changes:
- EditorPanel.vue: Import and render StylesheetViewer in code tab
- EditorPanel.vue: Add flexbox layout for proper height management
- StylesheetViewer.vue: Convert from fixed positioning to flex container
- App.vue: Remove standalone StylesheetViewer component

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:34:33 +01:00
isUnknown
19077fb133 refactor: migrate CSS to SCSS with compilation setup
- Move all CSS files to public/assets/css/src/ as SCSS
- Add public/assets/css/style.scss as main entry point
- Compile SCSS to public/assets/css/style.css
- Update src/style.css to import compiled CSS
- Add .css.map for source maps

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 16:57:11 +01:00
19 changed files with 851 additions and 209 deletions

7
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "geoproject", "name": "geoproject",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@melloware/coloris": "^0.25.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"pagedjs": "^0.4.3", "pagedjs": "^0.4.3",
"pinia": "^3.0.4", "pinia": "^3.0.4",
@ -532,6 +533,12 @@
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@melloware/coloris": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.25.0.tgz",
"integrity": "sha512-RBWVFLjWbup7GRkOXb9g3+ZtR9AevFtJinrRz2cYPLjZ3TCkNRGMWuNbmQWbZ5cF3VU7aQDZwUsYgIY/bGrh2g==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.50", "version": "1.0.0-beta.50",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",

View file

@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@melloware/coloris": "^0.25.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"pagedjs": "^0.4.3", "pagedjs": "^0.4.3",
"pinia": "^3.0.4", "pinia": "^3.0.4",

View file

@ -1 +0,0 @@

View file

@ -0,0 +1,21 @@
button {
cursor: pointer;
border: 1px solid var(--color-browngray-300);
color: var(--color-browngray-300);
border-radius: var(--border-radius);
padding: 0.1rem 0.3rem;
&.active {
border: 1px solid #000;
color: #000;
}
&.tab {
&.active {
background-color: #000;
color: #fff;
border: none;
}
}
}

View file

@ -0,0 +1,72 @@
select,
input[type="text"],
input[type="number"] {
background-color: var(--color-browngray-300);
}
.field--view-only {
opacity: 0.3;
}
.settings-section {
h2 {
border-bottom: 1px solid #000;
}
.settings-subsection:not(:last-child) {
border-bottom: 1px solid var(--color-browngray-050);
}
.settings-subsection {
padding: 0.5rem 0;
.field {
display: flex;
label {
width: 50%;
}
.input-with-unit {
display: flex;
}
.input-with-color {
.clr-field {
display: flex;
button {
position: absolute;
transform: none;
height: 1rem;
top: auto;
right: auto;
cursor: pointer;
}
input {
padding-left: 2.5rem;
}
}
}
}
&.margins {
display: flex;
flex-wrap: wrap;
h3 {
width: 100%;
}
.field {
width: 50%;
label {
width: 50%;
}
.input-with-unit {
input {
width: 50%;
}
}
}
}
}
}

View file

@ -0,0 +1,27 @@
body,
html {
padding: 0;
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}
input,
select {
border: none;
outline: none;
border-radius: var(--border-radius);
}
button {
background-color: transparent;
border: none;
}

View file

@ -0,0 +1,15 @@
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
a,
input,
select,
figcaption,
label {
font-family: sans-serif;
}

View file

@ -0,0 +1,8 @@
:root {
--color-panel-bg: #e8e6e5;
--color-browngray-050: #f5f3f0;
--color-browngray-200: #d0c4ba;
--color-browngray-300: #b5a9a1;
--border-radius: 0.2rem;
}

145
public/assets/css/style.css Normal file
View file

@ -0,0 +1,145 @@
body,
html {
padding: 0;
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}
input,
select {
border: none;
outline: none;
border-radius: var(--border-radius);
}
button {
background-color: transparent;
border: none;
}
:root {
--color-panel-bg: #e8e6e5;
--color-browngray-050: #f5f3f0;
--color-browngray-200: #d0c4ba;
--color-browngray-300: #b5a9a1;
--border-radius: 0.2rem;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
a,
input,
select,
figcaption,
label {
font-family: sans-serif;
}
/* PagedJS print styles */
@page {
size: A4;
margin: 20mm 15mm 26mm 15mm;
}
h2 {
-moz-column-break-before: page;
break-before: page;
}
@page {
@bottom-center {
content: string(title);
}
}
.chapter > h2 {
string-set: title content(text);
}
select,
input[type=text],
input[type=number] {
background-color: var(--color-browngray-300);
}
.field--view-only {
opacity: 0.3;
}
.settings-section h2 {
border-bottom: 1px solid #000;
}
.settings-section .settings-subsection:not(:last-child) {
border-bottom: 1px solid var(--color-browngray-050);
}
.settings-section .settings-subsection {
padding: 0.5rem 0;
}
.settings-section .settings-subsection .field {
display: flex;
}
.settings-section .settings-subsection .field label {
width: 50%;
}
.settings-section .settings-subsection .field .input-with-unit {
display: flex;
}
.settings-section .settings-subsection .field .input-with-color .clr-field {
display: flex;
}
.settings-section .settings-subsection .field .input-with-color .clr-field button {
position: absolute;
transform: none;
height: 1rem;
top: auto;
right: auto;
cursor: pointer;
}
.settings-section .settings-subsection .field .input-with-color .clr-field input {
padding-left: 2.5rem;
}
.settings-section .settings-subsection.margins {
display: flex;
flex-wrap: wrap;
}
.settings-section .settings-subsection.margins h3 {
width: 100%;
}
.settings-section .settings-subsection.margins .field {
width: 50%;
}
.settings-section .settings-subsection.margins .field label {
width: 50%;
}
.settings-section .settings-subsection.margins .field .input-with-unit input {
width: 50%;
}
button {
cursor: pointer;
border: 1px solid var(--color-browngray-300);
color: var(--color-browngray-300);
border-radius: var(--border-radius);
padding: 0.1rem 0.3rem;
}
button.active {
border: 1px solid #000;
color: #000;
}
button.tab.active {
background-color: #000;
color: #fff;
border: none;
}/*# sourceMappingURL=style.css.map */

View file

@ -0,0 +1 @@
{"version":3,"sources":["src/_reset.scss","style.css","src/_variables.scss","src/_text.scss","src/_print-styles.scss","src/_forms.scss","src/_buttons.scss"],"names":[],"mappings":"AAAA;;EAEE,UAAA;EACA,SAAA;ACCF;;ADEA;;;;;;EAME,SAAA;ACCF;;ADEA;;EAEE,YAAA;EACA,aAAA;EAEA,mCAAA;ACAF;;ADGA;EACE,6BAAA;EACA,YAAA;ACAF;;ACzBA;EACE,yBAAA;EACA,8BAAA;EACA,8BAAA;EACA,8BAAA;EAEA,uBAAA;AD2BF;;AEjCA;;;;;;;;;;;;;EAaE,uBAAA;AFoCF;;AGjDA,yBAAA;AACA;EACE,QAAA;EACA,2BAAA;AHoDF;AGlDA;EACE,8BAAA;OAAA,kBAAA;AHoDF;;AGjDA;EACE;IACE,sBAAA;EHoDF;AACF;AGlDA;EACE,+BAAA;AHoDF;;AInEA;;;EAGE,4CAAA;AJsEF;;AInEA;EACE,YAAA;AJsEF;;AIlEE;EACE,6BAAA;AJqEJ;AInEE;EACE,mDAAA;AJqEJ;AIlEE;EACE,iBAAA;AJoEJ;AIlEI;EACE,aAAA;AJoEN;AInEM;EACE,UAAA;AJqER;AIlEM;EACE,aAAA;AJoER;AIhEQ;EACE,aAAA;AJkEV;AIjEU;EACE,kBAAA;EACA,eAAA;EACA,YAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;AJmEZ;AIhEU;EACE,oBAAA;AJkEZ;AI5DI;EACE,aAAA;EACA,eAAA;AJ8DN;AI7DM;EACE,WAAA;AJ+DR;AI7DM;EACE,UAAA;AJ+DR;AI7DQ;EACE,UAAA;AJ+DV;AI3DU;EACE,UAAA;AJ6DZ;;AK9HA;EACE,eAAA;EAEA,4CAAA;EACA,iCAAA;EACA,mCAAA;EACA,sBAAA;ALgIF;AK9HE;EACE,sBAAA;EACA,WAAA;ALgIJ;AK5HI;EACE,sBAAA;EACA,WAAA;EACA,YAAA;AL8HN","file":"style.css"}

View file

@ -0,0 +1,6 @@
@import "src/_reset.scss";
@import "src/_variables.scss";
@import "src/_text.scss";
@import "src/_print-styles.scss";
@import "src/_forms.scss";
@import "src/_buttons.scss";

View file

@ -8,8 +8,8 @@
<?= e($page->isHomePage() != true, $page->title() . ' - ') . $site->title() ?> <?= e($page->isHomePage() != true, $page->title() . ' - ') . $site->title() ?>
</title> </title>
<link rel="stylesheet" href="<?= url('assets/pagedjs-interface.css') ?>"> <link rel="stylesheet" href="<?= url('assets/css/style..css') ?>">
<link rel="stylesheet" href="<?= url('assets/stylesheet.css') ?>"> <link rel="stylesheet" href="<?= url('assets/css/pagedjs-interface.css') ?>">
<!-- À SUPPRIMER EN PRODUCTION --> <!-- À SUPPRIMER EN PRODUCTION -->
<meta name="robots" content="noindex, nofollow, noarchive"> <meta name="robots" content="noindex, nofollow, noarchive">

View file

@ -1,7 +1,6 @@
<script setup> <script setup>
import PagedJsWrapper from './components/PagedJsWrapper.vue'; import PagedJsWrapper from './components/PagedJsWrapper.vue';
import EditorPanel from './components/editor/EditorPanel.vue'; import EditorPanel from './components/editor/EditorPanel.vue';
import StylesheetViewer from './components/StylesheetViewer.vue';
import ElementPopup from './components/ElementPopup.vue'; import ElementPopup from './components/ElementPopup.vue';
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useStylesheetStore } from './stores/stylesheet'; import { useStylesheetStore } from './stores/stylesheet';
@ -42,7 +41,10 @@ const renderPreview = async (shouldReloadFromFile = false) => {
`; `;
iframe.onload = () => { iframe.onload = () => {
iframe.contentDocument.addEventListener('click', elementPopup.value.handleIframeClick); iframe.contentDocument.addEventListener(
'click',
elementPopup.value.handleIframeClick
);
setTimeout(() => { setTimeout(() => {
const scrollHeight = iframe.contentDocument.documentElement.scrollHeight; const scrollHeight = iframe.contentDocument.documentElement.scrollHeight;
@ -55,9 +57,12 @@ const renderPreview = async (shouldReloadFromFile = false) => {
}; };
}; };
watch(() => stylesheetStore.content, () => { watch(
() => stylesheetStore.content,
() => {
renderPreview(); renderPreview();
}); }
);
onMounted(() => renderPreview(true)); onMounted(() => renderPreview(true));
</script> </script>
@ -71,8 +76,6 @@ onMounted(() => renderPreview(true));
<iframe ref="previewFrame" id="preview-frame"></iframe> <iframe ref="previewFrame" id="preview-frame"></iframe>
<StylesheetViewer :stylesheet="stylesheetStore.content" />
<ElementPopup ref="elementPopup" :iframeRef="previewFrame" /> <ElementPopup ref="elementPopup" :iframeRef="previewFrame" />
</template> </template>
@ -80,8 +83,8 @@ onMounted(() => renderPreview(true));
#preview-frame { #preview-frame {
position: fixed; position: fixed;
top: 0; top: 0;
left: 250px; left: 0;
width: calc(100% - 600px); width: 100vw;
height: 100vh; height: 100vh;
border: none; border: none;
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<aside id="stylesheet-viewer"> <div id="stylesheet-viewer">
<div class="header"> <div class="header">
<h3>Stylesheet</h3> <h3>Stylesheet</h3>
<label class="toggle"> <label class="toggle">
@ -17,7 +17,7 @@
@input="handleInput" @input="handleInput"
spellcheck="false" spellcheck="false"
></textarea> ></textarea>
</aside> </div>
</template> </template>
<script setup> <script setup>
@ -53,14 +53,9 @@ const handleInput = (event) => {
<style scoped> <style scoped>
#stylesheet-viewer { #stylesheet-viewer {
position: fixed; display: flex;
top: 0; flex-direction: column;
right: 0; height: 100%;
width: 350px;
height: 100vh;
padding: 1rem;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
overflow-y: auto;
background: #282c34; background: #282c34;
color: #fff; color: #fff;
} }
@ -130,7 +125,7 @@ h3 {
.readonly { .readonly {
margin: 0; margin: 0;
height: calc(100vh - 5rem); flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 0.5rem; padding: 0.5rem;
background: #1e1e1e; background: #1e1e1e;
@ -145,7 +140,7 @@ h3 {
textarea { textarea {
width: 100%; width: 100%;
height: calc(100vh - 5rem); flex: 1;
background: #1e1e1e; background: #1e1e1e;
color: #abb2bf; color: #abb2bf;
border: none; border: none;

View file

@ -3,6 +3,7 @@
<nav class="tabs"> <nav class="tabs">
<button <button
type="button" type="button"
class="tab"
:class="{ active: activeTab === 'document' }" :class="{ active: activeTab === 'document' }"
@click="activeTab = 'document'" @click="activeTab = 'document'"
> >
@ -10,6 +11,7 @@
</button> </button>
<button <button
type="button" type="button"
class="tab"
:class="{ active: activeTab === 'code' }" :class="{ active: activeTab === 'code' }"
@click="activeTab = 'code'" @click="activeTab = 'code'"
> >
@ -17,6 +19,7 @@
</button> </button>
<button <button
type="button" type="button"
class="tab"
:class="{ active: activeTab === 'contenu' }" :class="{ active: activeTab === 'contenu' }"
@click="activeTab = 'contenu'" @click="activeTab = 'contenu'"
> >
@ -31,7 +34,7 @@
</div> </div>
<div v-else-if="activeTab === 'code'" class="tab-panel"> <div v-else-if="activeTab === 'code'" class="tab-panel">
<!-- Code tab content --> <StylesheetViewer />
</div> </div>
<div v-else-if="activeTab === 'contenu'" class="tab-panel"> <div v-else-if="activeTab === 'contenu'" class="tab-panel">
@ -42,43 +45,48 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, provide } from 'vue';
import PageSettings from './PageSettings.vue'; import PageSettings from './PageSettings.vue';
import TextSettings from './TextSettings.vue'; import TextSettings from './TextSettings.vue';
import StylesheetViewer from '../StylesheetViewer.vue';
// Tab management // Tab management
const activeTab = ref('document'); const activeTab = ref('document');
// Provide activeTab to child components
provide('activeTab', activeTab);
</script> </script>
<style scoped> <style lang="scss" scoped>
#editor-panel { #editor-panel {
padding: 1rem;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 250px; width: 30rem;
height: 100vh; height: 100vh;
background: #f5f5f5; display: flex;
padding: 1rem; flex-direction: column;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); z-index: 2;
background-color: var(--color-panel-bg);
box-shadow: -5px 0px 12px;
}
nav {
margin-bottom: 2rem;
display: flex;
gap: 0.5rem;
}
.tab-content {
flex: 1;
overflow: hidden;
}
.tab-panel {
height: 100%;
overflow-y: auto; 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> </style>

View file

@ -2,6 +2,7 @@
<section class="settings-section"> <section class="settings-section">
<h2>Réglage des pages</h2> <h2>Réglage des pages</h2>
<div class="settings-subsection">
<div class="field"> <div class="field">
<label for="page-format">Format d'impression</label> <label for="page-format">Format d'impression</label>
<select id="page-format" v-model="pageFormat"> <select id="page-format" v-model="pageFormat">
@ -12,33 +13,38 @@
<option value="legal">Legal</option> <option value="legal">Legal</option>
</select> </select>
</div> </div>
</div>
<div class="field"> <div class="settings-subsection">
<div class="field field--view-only">
<label for="page-width">Largeur</label> <label for="page-width">Largeur</label>
<input <input
id="page-width" id="page-width"
type="text" type="number"
:value="pageWidth" :value="parseInt(pageWidth)"
disabled disabled
/> />
<button type="button" disabled>mm</button>
</div> </div>
<div class="field"> <div class="field field--view-only">
<label for="page-height">Hauteur</label> <label for="page-height">Hauteur</label>
<input <input
id="page-height" id="page-height"
type="text" type="number"
:value="pageHeight" :value="parseInt(pageHeight)"
disabled disabled
/> />
<button type="button" disabled>mm</button>
</div>
</div> </div>
<div class="subsection"> <div class="settings-subsection margins">
<h3>Marges</h3> <h3>Marges</h3>
<div class="field"> <div class="field">
<label for="margin-top">Haut</label> <label for="margin-top">Haut</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-top" id="margin-top"
type="number" type="number"
@ -66,7 +72,7 @@
<div class="field"> <div class="field">
<label for="margin-bottom">Bas</label> <label for="margin-bottom">Bas</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-bottom" id="margin-bottom"
type="number" type="number"
@ -94,7 +100,7 @@
<div class="field"> <div class="field">
<label for="margin-left">Gauche</label> <label for="margin-left">Gauche</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-left" id="margin-left"
type="number" type="number"
@ -122,7 +128,7 @@
<div class="field"> <div class="field">
<label for="margin-right">Droite</label> <label for="margin-right">Droite</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-right" id="margin-right"
type="number" type="number"
@ -149,14 +155,18 @@
</div> </div>
</div> </div>
<div class="settings-subsection">
<div class="field"> <div class="field">
<label for="background">Arrière-plan</label> <label for="background">Arrière-plan</label>
<div class="field-with-unit"> <div class="input-with-color">
<input <input
id="background" ref="backgroundColorInput"
type="text" type="text"
id="background"
v-model="background.value" v-model="background.value"
data-coloris
/> />
<!-- Temporarily commented out
<div class="unit-toggle"> <div class="unit-toggle">
<button <button
type="button" type="button"
@ -173,9 +183,12 @@
hex hex
</button> </button>
</div> </div>
-->
</div>
</div> </div>
</div> </div>
<div class="settings-subsection">
<div class="field"> <div class="field">
<label for="pattern">Motif</label> <label for="pattern">Motif</label>
<select id="pattern" v-model="pattern"> <select id="pattern" v-model="pattern">
@ -185,32 +198,31 @@
<option value="grid">Grille</option> <option value="grid">Grille</option>
</select> </select>
</div> </div>
</div>
<div class="settings-subsection">
<div class="field checkbox-field"> <div class="field checkbox-field">
<input <input id="page-numbers" type="checkbox" v-model="pageNumbers" />
id="page-numbers"
type="checkbox"
v-model="pageNumbers"
/>
<label for="page-numbers">Numéro de page</label> <label for="page-numbers">Numéro de page</label>
</div> </div>
<div class="field checkbox-field"> <div class="field checkbox-field">
<input <input id="running-title" type="checkbox" v-model="runningTitle" />
id="running-title"
type="checkbox"
v-model="runningTitle"
/>
<label for="running-title">Titre courant</label> <label for="running-title">Titre courant</label>
</div> </div>
</div>
</section> </section>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted, inject } from 'vue';
import { useStylesheetStore } from '../../stores/stylesheet'; import { useStylesheetStore } from '../../stores/stylesheet';
import Coloris from '@melloware/coloris';
import '@melloware/coloris/dist/coloris.css';
const stylesheetStore = useStylesheetStore(); const stylesheetStore = useStylesheetStore();
const backgroundColorInput = ref(null);
const activeTab = inject('activeTab', ref('document'));
let isUpdatingFromStore = false; let isUpdatingFromStore = false;
let updateTimer = null; let updateTimer = null;
@ -222,7 +234,7 @@ const pageFormats = {
A5: { width: '148mm', height: '210mm' }, A5: { width: '148mm', height: '210mm' },
A3: { width: '297mm', height: '420mm' }, A3: { width: '297mm', height: '420mm' },
letter: { width: '8.5in', height: '11in' }, letter: { width: '8.5in', height: '11in' },
legal: { width: '8.5in', height: '14in' } legal: { width: '8.5in', height: '14in' },
}; };
const pageWidth = computed(() => pageFormats[pageFormat.value].width); const pageWidth = computed(() => pageFormats[pageFormat.value].width);
@ -232,12 +244,12 @@ const margins = ref({
top: { value: 20, unit: 'mm' }, top: { value: 20, unit: 'mm' },
bottom: { value: 20, unit: 'mm' }, bottom: { value: 20, unit: 'mm' },
left: { value: 20, unit: 'mm' }, left: { value: 20, unit: 'mm' },
right: { value: 20, unit: 'mm' } right: { value: 20, unit: 'mm' },
}); });
const background = ref({ const background = ref({
value: '', value: '',
format: 'hex' format: 'hex',
}); });
const pattern = ref(''); const pattern = ref('');
@ -249,19 +261,20 @@ const debouncedUpdate = (callback) => {
updateTimer = setTimeout(callback, 1000); updateTimer = setTimeout(callback, 1000);
}; };
const immediateUpdate = (callback) => {
callback();
};
watch(pageFormat, (newFormat) => { watch(pageFormat, (newFormat) => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
debouncedUpdate(() => { immediateUpdate(() => {
stylesheetStore.updateProperty('@page', 'size', newFormat, ''); stylesheetStore.updateProperty('@page', 'size', newFormat, '');
}); });
}); });
watch(margins, (newMargins) => { const updateMargins = () => {
if (isUpdatingFromStore) return; const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`;
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 currentBlock = stylesheetStore.extractBlock('@page');
const updatedBlock = currentBlock.replace( const updatedBlock = currentBlock.replace(
@ -269,37 +282,88 @@ watch(margins, (newMargins) => {
`$1${marginValue}` `$1${marginValue}`
); );
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock); stylesheetStore.content = stylesheetStore.content.replace(
}); currentBlock,
}, { deep: true }); updatedBlock
);
};
watch(background, (newBg) => { // Watch margin values (number inputs) with debounce
if (!newBg.value) return; watch(
() => [
margins.value.top.value,
margins.value.bottom.value,
margins.value.left.value,
margins.value.right.value,
],
() => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
debouncedUpdate(updateMargins);
}
);
// Watch margin units (button clicks) without debounce
watch(
() => [
margins.value.top.unit,
margins.value.bottom.unit,
margins.value.left.unit,
margins.value.right.unit,
],
() => {
if (isUpdatingFromStore) return;
immediateUpdate(updateMargins);
}
);
const updateBackground = () => {
if (!background.value.value) return;
debouncedUpdate(() => {
const currentBlock = stylesheetStore.extractBlock('@page'); const currentBlock = stylesheetStore.extractBlock('@page');
if (currentBlock.includes('background:')) { if (currentBlock.includes('background:')) {
const updatedBlock = currentBlock.replace( const updatedBlock = currentBlock.replace(
/(background:\s*)[^;]+/, /(background:\s*)[^;]+/,
`$1${newBg.value}` `$1${background.value.value}`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
); );
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
} else { } else {
const updatedBlock = currentBlock.replace( const updatedBlock = currentBlock.replace(
/(\s*})$/, /(\s*})$/,
` background: ${newBg.value};\n$1` ` background: ${background.value.value};\n$1`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
); );
stylesheetStore.content = stylesheetStore.content.replace(currentBlock, updatedBlock);
} }
}); };
}, { deep: true });
// Watch background value (text input) with debounce
watch(
() => background.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(updateBackground);
}
);
// Watch background format (button clicks) without debounce
watch(
() => background.value.format,
() => {
if (isUpdatingFromStore) return;
immediateUpdate(updateBackground);
}
);
watch(pattern, (newPattern) => { watch(pattern, (newPattern) => {
if (!newPattern || isUpdatingFromStore) return; if (!newPattern || isUpdatingFromStore) return;
debouncedUpdate(() => { immediateUpdate(() => {
// TODO: implement pattern application // TODO: implement pattern application
}); });
}); });
@ -307,7 +371,7 @@ watch(pattern, (newPattern) => {
watch(pageNumbers, (enabled) => { watch(pageNumbers, (enabled) => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
debouncedUpdate(() => { immediateUpdate(() => {
// TODO: implement page numbers toggle // TODO: implement page numbers toggle
}); });
}); });
@ -315,7 +379,7 @@ watch(pageNumbers, (enabled) => {
watch(runningTitle, (enabled) => { watch(runningTitle, (enabled) => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
debouncedUpdate(() => { immediateUpdate(() => {
// TODO: implement running title toggle // TODO: implement running title toggle
}); });
}); });
@ -331,12 +395,26 @@ const syncFromStore = () => {
pageFormat.value = sizeMatch[1]; 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); 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) { if (marginMatch) {
margins.value.top = { value: parseFloat(marginMatch[1]), unit: marginMatch[2] }; margins.value.top = {
margins.value.right = { value: parseFloat(marginMatch[3]), unit: marginMatch[4] }; value: parseFloat(marginMatch[1]),
margins.value.bottom = { value: parseFloat(marginMatch[5]), unit: marginMatch[6] }; unit: marginMatch[2],
margins.value.left = { value: parseFloat(marginMatch[7]), unit: marginMatch[8] }; };
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*([^;]+)/); const bgMatch = pageBlock.match(/background:\s*([^;]+)/);
@ -348,13 +426,67 @@ const syncFromStore = () => {
} }
}; };
watch(() => stylesheetStore.content, () => { watch(
() => stylesheetStore.content,
() => {
if (!isUpdatingFromStore) { if (!isUpdatingFromStore) {
syncFromStore(); syncFromStore();
} }
}
);
const updateColorisButton = () => {
const input = backgroundColorInput.value;
if (input && background.value.value) {
// Force Coloris to update by triggering a change event
const event = new Event('input', { bubbles: true });
input.dispatchEvent(event);
}
};
// Watch for when the user returns to the "document" tab
watch(activeTab, (newTab, oldTab) => {
if (newTab === 'document' && oldTab !== 'document' && background.value.value) {
// Small delay to ensure DOM is ready
setTimeout(updateColorisButton, 100);
}
}); });
onMounted(() => { onMounted(() => {
syncFromStore(); syncFromStore();
// Initialize Coloris
Coloris.init();
Coloris({
el: '[data-coloris]',
theme: 'pill',
themeMode: 'dark',
formatToggle: true,
alpha: true,
closeButton: true,
closeLabel: 'Fermer',
clearButton: true,
clearLabel: 'Effacer',
swatchesOnly: false,
inline: false,
wrap: true,
swatches: [
'#264653',
'#2a9d8f',
'#e9c46a',
'#f4a261',
'#e76f51',
'#d62828',
'#023e8a',
'#0077b6',
'#ffffff',
'#000000',
],
});
// Initialize button color if value exists
if (background.value.value) {
setTimeout(updateColorisButton, 100);
}
}); });
</script> </script>

View file

@ -2,8 +2,8 @@
<section class="settings-section"> <section class="settings-section">
<h2>Réglage du texte</h2> <h2>Réglage du texte</h2>
<p class="infos"> <p class="infos">
Ces réglages s'appliquent à l'ensemble des éléments du document. Ces réglages s'appliquent à l'ensemble des éléments du document. Vous
Vous pouvez modifier ensuite les éléments indépendamment. pouvez modifier ensuite les éléments indépendamment.
</p> </p>
<div class="field"> <div class="field">
@ -17,11 +17,7 @@
<option value="Times New Roman">Times New Roman</option> <option value="Times New Roman">Times New Roman</option>
</select> </select>
<div class="field-checkbox"> <div class="field-checkbox">
<input <input id="text-italic" type="checkbox" v-model="italic" />
id="text-italic"
type="checkbox"
v-model="italic"
/>
<label for="text-italic">Italique</label> <label for="text-italic">Italique</label>
</div> </div>
</div> </div>
@ -84,7 +80,7 @@
<div class="field"> <div class="field">
<label for="text-size-range">Taille du texte</label> <label for="text-size-range">Taille du texte</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="text-size-range" id="text-size-range"
type="range" type="range"
@ -140,11 +136,7 @@
<div class="field"> <div class="field">
<label for="text-color">Couleur</label> <label for="text-color">Couleur</label>
<div class="field-with-color"> <div class="field-with-color">
<input <input type="color" v-model="color.picker" class="color-picker" />
type="color"
v-model="color.picker"
class="color-picker"
/>
<input <input
id="text-color" id="text-color"
type="text" type="text"
@ -228,7 +220,7 @@
<div class="field"> <div class="field">
<label for="margin-outer">Marges extérieures</label> <label for="margin-outer">Marges extérieures</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-outer" id="margin-outer"
type="number" type="number"
@ -266,7 +258,7 @@
<div v-if="marginOuterExpanded" class="subsection collapsed-section"> <div v-if="marginOuterExpanded" class="subsection collapsed-section">
<div class="field"> <div class="field">
<label for="margin-outer-top">Haut</label> <label for="margin-outer-top">Haut</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-outer-top" id="margin-outer-top"
type="number" type="number"
@ -294,7 +286,7 @@
<div class="field"> <div class="field">
<label for="margin-outer-bottom">Bas</label> <label for="margin-outer-bottom">Bas</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-outer-bottom" id="margin-outer-bottom"
type="number" type="number"
@ -322,7 +314,7 @@
<div class="field"> <div class="field">
<label for="margin-outer-left">Gauche</label> <label for="margin-outer-left">Gauche</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-outer-left" id="margin-outer-left"
type="number" type="number"
@ -350,7 +342,7 @@
<div class="field"> <div class="field">
<label for="margin-outer-right">Droite</label> <label for="margin-outer-right">Droite</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-outer-right" id="margin-outer-right"
type="number" type="number"
@ -379,7 +371,7 @@
<div class="field"> <div class="field">
<label for="margin-inner">Marges intérieures</label> <label for="margin-inner">Marges intérieures</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-inner" id="margin-inner"
type="number" type="number"
@ -417,7 +409,7 @@
<div v-if="marginInnerExpanded" class="subsection collapsed-section"> <div v-if="marginInnerExpanded" class="subsection collapsed-section">
<div class="field"> <div class="field">
<label for="margin-inner-top">Haut</label> <label for="margin-inner-top">Haut</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-inner-top" id="margin-inner-top"
type="number" type="number"
@ -445,7 +437,7 @@
<div class="field"> <div class="field">
<label for="margin-inner-bottom">Bas</label> <label for="margin-inner-bottom">Bas</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-inner-bottom" id="margin-inner-bottom"
type="number" type="number"
@ -473,7 +465,7 @@
<div class="field"> <div class="field">
<label for="margin-inner-left">Gauche</label> <label for="margin-inner-left">Gauche</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-inner-left" id="margin-inner-left"
type="number" type="number"
@ -501,7 +493,7 @@
<div class="field"> <div class="field">
<label for="margin-inner-right">Droite</label> <label for="margin-inner-right">Droite</label>
<div class="field-with-unit"> <div class="input-with-unit">
<input <input
id="margin-inner-right" id="margin-inner-right"
type="number" type="number"
@ -531,7 +523,22 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, watch } from 'vue';
import { useStylesheetStore } from '../../stores/stylesheet';
const stylesheetStore = useStylesheetStore();
let isUpdatingFromStore = false;
let updateTimer = null;
const debouncedUpdate = (callback) => {
clearTimeout(updateTimer);
updateTimer = setTimeout(callback, 1000);
};
const immediateUpdate = (callback) => {
callback();
};
// Font // Font
const font = ref('Alegreya Sans'); const font = ref('Alegreya Sans');
@ -543,7 +550,7 @@ const weight = ref('400');
// Font size // Font size
const fontSize = ref({ const fontSize = ref({
value: 23, value: 23,
unit: 'px' unit: 'px',
}); });
// Alignment // Alignment
@ -553,7 +560,7 @@ const alignment = ref('left');
const color = ref({ const color = ref({
picker: '#000000', picker: '#000000',
value: 'rgb(250, 250, 250)', value: 'rgb(250, 250, 250)',
format: 'rgb' format: 'rgb',
}); });
const clearColor = () => { const clearColor = () => {
@ -565,7 +572,7 @@ const clearColor = () => {
const background = ref({ const background = ref({
enabled: false, enabled: false,
value: 'transparent', value: 'transparent',
format: 'hex' format: 'hex',
}); });
const clearBackground = () => { const clearBackground = () => {
@ -575,7 +582,7 @@ const clearBackground = () => {
// Margin outer // Margin outer
const marginOuter = ref({ const marginOuter = ref({
value: 23, value: 23,
unit: 'mm' unit: 'mm',
}); });
const marginOuterExpanded = ref(false); const marginOuterExpanded = ref(false);
@ -584,13 +591,13 @@ const marginOuterDetailed = ref({
top: { value: 23, unit: 'mm' }, top: { value: 23, unit: 'mm' },
bottom: { value: 23, unit: 'mm' }, bottom: { value: 23, unit: 'mm' },
left: { value: 23, unit: 'mm' }, left: { value: 23, unit: 'mm' },
right: { value: 23, unit: 'mm' } right: { value: 23, unit: 'mm' },
}); });
// Margin inner // Margin inner
const marginInner = ref({ const marginInner = ref({
value: 23, value: 23,
unit: 'mm' unit: 'mm',
}); });
const marginInnerExpanded = ref(false); const marginInnerExpanded = ref(false);
@ -599,6 +606,202 @@ const marginInnerDetailed = ref({
top: { value: 23, unit: 'mm' }, top: { value: 23, unit: 'mm' },
bottom: { value: 23, unit: 'mm' }, bottom: { value: 23, unit: 'mm' },
left: { value: 23, unit: 'mm' }, left: { value: 23, unit: 'mm' },
right: { value: 23, unit: 'mm' } right: { value: 23, unit: 'mm' },
}); });
// Watchers - Immediate updates for select/buttons/checkboxes
watch(font, () => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement font update
});
});
watch(italic, () => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement italic update
});
});
watch(weight, () => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement weight update
});
});
watch(alignment, () => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement alignment update
});
});
// Font size - debounced for value, immediate for unit
watch(
() => fontSize.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
// TODO: implement font size update
});
}
);
watch(
() => fontSize.value.unit,
() => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement font size update
});
}
);
// Color - debounced for text value, immediate for format and picker
watch(
() => color.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
// TODO: implement color update
});
}
);
watch(
() => [color.value.format, color.value.picker],
() => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement color update
});
}
);
// Background - debounced for value, immediate for format and enabled
watch(
() => background.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
// TODO: implement background update
});
}
);
watch(
() => [background.value.format, background.value.enabled],
() => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement background update
});
}
);
// Margin outer - debounced for value, immediate for unit
watch(
() => marginOuter.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
// TODO: implement margin outer update
});
}
);
watch(
() => marginOuter.value.unit,
() => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement margin outer update
});
}
);
// Margin outer detailed - debounced for values, immediate for units
watch(
() => [
marginOuterDetailed.value.top.value,
marginOuterDetailed.value.bottom.value,
marginOuterDetailed.value.left.value,
marginOuterDetailed.value.right.value,
],
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
// TODO: implement margin outer detailed update
});
}
);
watch(
() => [
marginOuterDetailed.value.top.unit,
marginOuterDetailed.value.bottom.unit,
marginOuterDetailed.value.left.unit,
marginOuterDetailed.value.right.unit,
],
() => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement margin outer detailed update
});
}
);
// Margin inner - debounced for value, immediate for unit
watch(
() => marginInner.value.value,
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
// TODO: implement margin inner update
});
}
);
watch(
() => marginInner.value.unit,
() => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement margin inner update
});
}
);
// Margin inner detailed - debounced for values, immediate for units
watch(
() => [
marginInnerDetailed.value.top.value,
marginInnerDetailed.value.bottom.value,
marginInnerDetailed.value.left.value,
marginInnerDetailed.value.right.value,
],
() => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
// TODO: implement margin inner detailed update
});
}
);
watch(
() => [
marginInnerDetailed.value.top.unit,
marginInnerDetailed.value.bottom.unit,
marginInnerDetailed.value.left.unit,
marginInnerDetailed.value.right.unit,
],
() => {
if (isUpdatingFromStore) return;
immediateUpdate(() => {
// TODO: implement margin inner detailed update
});
}
);
</script> </script>

View file

@ -1,2 +1 @@
@import url('/assets/css/pagedjs-interface.css'); @import url('/assets/css/style.css');
@import url('/assets/css/editor-ui.css');