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>
This commit is contained in:
isUnknown 2025-12-04 15:03:29 +01:00
parent 467ae905bd
commit 9af36fb422
17 changed files with 359 additions and 179 deletions

7
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "geoproject",
"version": "0.0.0",
"dependencies": {
"@melloware/coloris": "^0.25.0",
"highlight.js": "^11.11.1",
"pagedjs": "^0.4.3",
"pinia": "^3.0.4",
@ -532,6 +533,12 @@
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"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": {
"version": "1.0.0-beta.50",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",

View file

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

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;
}

View file

@ -1 +0,0 @@

View file

@ -1,152 +1,52 @@
@charset "UTF-8";
/* CSS for Paged.js interface v0.4 */
/* Change the look */
:root {
--color-background: whitesmoke;
--color-pageSheet: #cfcfcf;
--color-pageBox: violet;
--color-paper: white;
--color-marginBox: transparent;
--pagedjs-crop-color: black;
--pagedjs-crop-shadow: white;
--pagedjs-crop-stroke: 1px;
}
/* To define how the book look on the screen: */
@media screen, pagedjs-ignore {
body {
background-color: var(--color-background);
}
.pagedjs_pages {
display: flex;
width: calc(var(--pagedjs-width) * 2);
flex: 0;
flex-wrap: wrap;
margin: 0 auto;
}
.pagedjs_page {
background-color: var(--color-paper);
box-shadow: 0 0 0 1px var(--color-pageSheet);
body,
html {
padding: 0;
margin: 0;
flex-shrink: 0;
flex-grow: 0;
margin-top: 10mm;
}
.pagedjs_first_page {
margin-left: var(--pagedjs-width);
}
.pagedjs_page:last-of-type {
margin-bottom: 10mm;
}
.pagedjs_pagebox {
box-shadow: 0 0 0 1px var(--color-pageBox);
}
.pagedjs_left_page {
z-index: 20;
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width)) !important;
}
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop {
border-color: transparent;
}
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle {
width: 0;
}
.pagedjs_right_page {
z-index: 10;
position: relative;
left: calc(var(--pagedjs-bleed-left) * -1);
}
/* show the margin-box */
.pagedjs_margin-top-left-corner-holder,
.pagedjs_margin-top,
.pagedjs_margin-top-left,
.pagedjs_margin-top-center,
.pagedjs_margin-top-right,
.pagedjs_margin-top-right-corner-holder,
.pagedjs_margin-bottom-left-corner-holder,
.pagedjs_margin-bottom,
.pagedjs_margin-bottom-left,
.pagedjs_margin-bottom-center,
.pagedjs_margin-bottom-right,
.pagedjs_margin-bottom-right-corner-holder,
.pagedjs_margin-right,
.pagedjs_margin-right-top,
.pagedjs_margin-right-middle,
.pagedjs_margin-right-bottom,
.pagedjs_margin-left,
.pagedjs_margin-left-top,
.pagedjs_margin-left-middle,
.pagedjs_margin-left-bottom {
box-shadow: 0 0 0 1px inset var(--color-marginBox);
}
/* uncomment this part for recto/verso book : ------------------------------------ */
/*
.pagedjs_pages {
flex-direction: column;
width: 100%;
}
.pagedjs_first_page {
margin-left: 0;
}
.pagedjs_page {
margin: 0 auto;
margin-top: 10mm;
}
.pagedjs_left_page{
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width) + var(--pagedjs-bleed-left))!important;
}
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop{
border-color: var(--pagedjs-crop-color);
}
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle{
width: var(--pagedjs-cross-size)!important;
}
.pagedjs_right_page{
left: 0;
}
*/
/*--------------------------------------------------------------------------------------*/
/* uncomment this par to see the baseline : -------------------------------------------*/
/* .pagedjs_pagebox {
--pagedjs-baseline: 22px;
--pagedjs-baseline-position: 5px;
--pagedjs-baseline-color: cyan;
background: linear-gradient(transparent 0%, transparent calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) var(--pagedjs-baseline)), transparent;
background-size: 100% var(--pagedjs-baseline);
background-repeat: repeat-y;
background-position-y: var(--pagedjs-baseline-position);
} */
/*--------------------------------------------------------------------------------------*/
}
/* Marks (to delete when merge in paged.js) */
.pagedjs_marks-crop {
z-index: 999999999999;
}
.pagedjs_bleed-top .pagedjs_marks-crop,
.pagedjs_bleed-bottom .pagedjs_marks-crop {
box-shadow: 1px 0px 0px 0px var(--pagedjs-crop-shadow);
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}
.pagedjs_bleed-top .pagedjs_marks-crop:last-child,
.pagedjs_bleed-bottom .pagedjs_marks-crop:last-child {
box-shadow: -1px 0px 0px 0px var(--pagedjs-crop-shadow);
input,
select {
border: none;
outline: none;
border-radius: var(--border-radius);
}
.pagedjs_bleed-left .pagedjs_marks-crop,
.pagedjs_bleed-right .pagedjs_marks-crop {
box-shadow: 0px 1px 0px 0px var(--pagedjs-crop-shadow);
button {
background-color: transparent;
border: none;
}
.pagedjs_bleed-left .pagedjs_marks-crop:last-child,
.pagedjs_bleed-right .pagedjs_marks-crop:last-child {
box-shadow: 0px -1px 0px 0px var(--pagedjs-crop-shadow);
: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 */
@ -168,28 +68,78 @@ h2 {
string-set: title content(text);
}
@page {
size: A4;
margin: 20mm 15mm 26mm 15mm;
}
@page {
@bottom-center {
content: string(title);
}
}
h2 {
-moz-column-break-before: page;
break-before: page;
select,
input[type=text],
input[type=number] {
background-color: var(--color-browngray-300);
}
.chapter > h2 {
string-set: title content(text);
.field--view-only {
opacity: 0.3;
}
#chapter-2 {
font-size: 2rem;
.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%;
}
p {
font-size: 1rem;
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

@ -1 +1 @@
{"version":3,"sources":["style.css","src/pagedjs-interface.scss","src/print-styles.scss","src/stylesheet.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACAhB,sCAAA;AAEA,oBAAA;AACA;EACE,8BAAA;EACA,0BAAA;EACA,uBAAA;EACA,oBAAA;EACA,8BAAA;EACA,2BAAA;EACA,4BAAA;EACA,0BAAA;ADCF;;ACEA,+CAAA;AACA;EACE;IACE,yCAAA;EDCF;ECEA;IACE,aAAA;IACA,qCAAA;IACA,OAAA;IACA,eAAA;IACA,cAAA;EDAF;ECGA;IACE,oCAAA;IACA,4CAAA;IACA,SAAA;IACA,cAAA;IACA,YAAA;IACA,gBAAA;EDDF;ECIA;IACE,iCAAA;EDFF;ECKA;IACE,mBAAA;EDHF;ECMA;IACE,0CAAA;EDJF;ECOA;IACE,WAAA;IACA,gFAAA;EDLF;ECUA;IACE,yBAAA;EDRF;ECWA;IACE,QAAA;EDTF;ECYA;IACE,WAAA;IACA,kBAAA;IACA,0CAAA;EDVF;ECaA,wBAAA;EAEA;;;;;;;;;;;;;;;;;;;;IAoBE,kDAAA;EDZF;ECeA,oFAAA;EAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAAA;EAgCA,yFAAA;EAEA,wFAAA;EAEA;;;;;;;;MAAA;EAUA,yFAAA;ADlBF;ACqBA,6CAAA;AAEA;EACE,qBAAA;ADpBF;;ACuBA;;EAEE,sDAAA;ADpBF;;ACuBA;;EAEE,uDAAA;ADpBF;;ACuBA;;EAEE,sDAAA;ADpBF;;ACuBA;;EAEE,uDAAA;ADpBF;;AErJA,yBAAA;AACA;EACE,QAAA;EACA,2BAAA;AFwJF;AEtJA;EACE,8BAAA;OAAA,kBAAA;AFwJF;;AErJA;EACE;IACE,sBAAA;EFwJF;AACF;AEtJA;EACE,+BAAA;AFwJF;;AGvKA;EACE,QAAA;EACA,2BAAA;AH0KF;AGvKA;EACE;IAAiB,sBAAA;EH0KjB;AACF;AGxKA;EACE,8BAAA;OAAA,kBAAA;AH0KF;;AGvKA;EACE,+BAAA;AH0KF;;AGvKA;EACE,eAAA;AH0KF;;AGvKA;EACE,eAAA;AH0KF","file":"style.css"}
{"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

@ -1,4 +1,6 @@
@import "src/editor-ui.scss";
@import "src/pagedjs-interface.scss";
@import "src/print-styles.scss";
@import "src/stylesheet.scss";
@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() ?>
</title>
<link rel="stylesheet" href="<?= url('assets/pagedjs-interface.css') ?>">
<link rel="stylesheet" href="<?= url('assets/stylesheet.css') ?>">
<link rel="stylesheet" href="<?= url('assets/css/style..css') ?>">
<link rel="stylesheet" href="<?= url('assets/css/pagedjs-interface.css') ?>">
<!-- À SUPPRIMER EN PRODUCTION -->
<meta name="robots" content="noindex, nofollow, noarchive">

View file

@ -45,13 +45,16 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, provide } from 'vue';
import PageSettings from './PageSettings.vue';
import TextSettings from './TextSettings.vue';
import StylesheetViewer from '../StylesheetViewer.vue';
// Tab management
const activeTab = ref('document');
// Provide activeTab to child components
provide('activeTab', activeTab);
</script>
<style lang="scss" scoped>
@ -60,7 +63,7 @@ const activeTab = ref('document');
position: fixed;
top: 0;
left: 0;
width: 25rem;
width: 30rem;
height: 100vh;
display: flex;
flex-direction: column;

View file

@ -18,12 +18,24 @@
<div class="settings-subsection">
<div class="field field--view-only">
<label for="page-width">Largeur</label>
<input id="page-width" type="text" :value="pageWidth" disabled />
<input
id="page-width"
type="number"
:value="parseInt(pageWidth)"
disabled
/>
<button type="button" disabled>mm</button>
</div>
<div class="field field--view-only">
<label for="page-height">Hauteur</label>
<input id="page-height" type="text" :value="pageHeight" disabled />
<input
id="page-height"
type="number"
:value="parseInt(pageHeight)"
disabled
/>
<button type="button" disabled>mm</button>
</div>
</div>
@ -146,8 +158,15 @@
<div class="settings-subsection">
<div class="field">
<label for="background">Arrière-plan</label>
<div class="input-with-unit">
<input id="background" type="text" v-model="background.value" />
<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"
@ -164,6 +183,7 @@
hex
</button>
</div>
-->
</div>
</div>
</div>
@ -195,10 +215,14 @@
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { ref, computed, watch, onMounted, inject } from 'vue';
import { useStylesheetStore } from '../../stores/stylesheet';
import Coloris from '@melloware/coloris';
import '@melloware/coloris/dist/coloris.css';
const stylesheetStore = useStylesheetStore();
const backgroundColorInput = ref(null);
const activeTab = inject('activeTab', ref('document'));
let isUpdatingFromStore = false;
let updateTimer = null;
@ -411,7 +435,58 @@ watch(
}
);
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(() => {
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>