Compare commits

...

4 commits

Author SHA1 Message Date
isUnknown
052c6958f3 feat: configure CI/CD with Forgejo Actions
Some checks failed
Deploy / Build and Deploy to Production (push) Failing after 26s
- Add Forgejo workflow for automated build and deploy
- Build creates dist/ from public/ then adds Vue app build
- Configure Vite to build to dist/assets/dist with fixed filenames
- Deploy entire dist/ directory to production via FTP
- Add workflow documentation with FTP setup instructions

The workflow mirrors the GitLab CI approach: dist/ is created during build
and synchronized to production root on every push to main.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 13:39:32 +01:00
isUnknown
5fb9cf68a3 merge 2025-12-11 13:39:23 +01:00
isUnknown
4ae4a6d509 merge 2025-12-11 13:38:27 +01:00
isUnknown
06aef5beb3 refactor: replace MarginEditor with linked margin fields in TextSettings
- Replace MarginEditor component with individual fields (top/bottom/left/right)
- Add link/unlink button with SVG icons to sync margin values
- When linked, all fields share the same value
- Auto-detect linked state when loading from stylesheet
- Match PageSettings UI pattern for consistency

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 13:37:59 +01:00
16 changed files with 643 additions and 204 deletions

View file

@ -0,0 +1,43 @@
# CI/CD avec Forgejo
## Vue d'ensemble
Le workflow `deploy.yml` automatise le build de l'application Vue et le déploiement sur le serveur de production.
## Workflow
À chaque push sur la branche `main` :
1. **Checkout** : Clone le dépôt
2. **Setup Node.js** : Installe Node.js 20
3. **Install dependencies** : Installe les dépendances npm
4. **Build Vue app** : Compile l'application Vue vers `public/assets/dist/`
5. **Deploy via FTP** : Synchronise les fichiers vers le serveur de production
## Configuration des secrets
Dans Forgejo, configurez les secrets suivants (Settings > Secrets and Variables > Actions) :
- `USERNAME` : Nom d'utilisateur FTP
- `PASSWORD` : Mot de passe FTP
- `PRODUCTION_HOST` : Hôte FTP (format : `ftp://host.example.com`)
## Fichiers déployés
Le workflow déploie depuis le dossier `public/` :
- `public/assets/``assets/` (incluant le build Vue dans `assets/dist/`)
- `public/site/``site/` (excluant accounts/, cache/, sessions/)
- `public/kirby/``kirby/`
- `public/vendor/``vendor/`
- `public/index.php``index.php`
## Build local
Pour tester le build localement :
```bash
npm run build
```
Les fichiers seront générés dans `public/assets/dist/` (ignorés par git).

View file

@ -0,0 +1,65 @@
name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Build and Deploy to Production
runs-on: docker
steps:
- name: Checkout code
run: |
git clone --depth 1 --branch main https://forge.studio-variable.com/${{ github.repository }}.git .
ls -la
- name: Setup Node.js
run: |
apt-get update -qq && apt-get install -y -qq curl
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y -qq nodejs
node --version
npm --version
- name: Install dependencies
run: |
npm ci
- name: Prepare dist directory
run: |
rm -rf dist
cp -r public dist
ls -la dist/
- name: Build Vue app
run: |
npm run build
ls -la dist/assets/dist/
- name: Deploy via FTP
env:
USERNAME: ${{ secrets.USERNAME }}
PASSWORD: ${{ secrets.PASSWORD }}
PRODUCTION_HOST: ${{ secrets.PRODUCTION_HOST }}
run: |
apt-get install -y -qq lftp
cd dist
lftp -c "
set ftp:ssl-allow no;
open -u $USERNAME,$PASSWORD $PRODUCTION_HOST;
mirror --reverse --verbose --ignore-time --parallel=10 \
-x 'local/' \
assets assets;
mirror --reverse --verbose --ignore-time --parallel=10 \
-x 'accounts/' \
-x 'cache/' \
-x 'sessions/' \
site site;
mirror --reverse --verbose --ignore-time --parallel=10 \
kirby kirby;
mirror --reverse --verbose --ignore-time --parallel=10 \
vendor vendor;
put index.php -o index.php;
quit"

View file

@ -1,11 +1,5 @@
.settings-section {
margin-top: 3em;
// .cons
margin: var(--space-m) 0;
h2 {
margin-bottom: var(--space);
@ -14,30 +8,25 @@
font-size: 1.4rem;
border-bottom: 1px solid var(--color-200);
color: var(--color-800);
}
.infos{
.infos {
font-size: 0.8rem;
color: var(--color-interface-400);
}
.settings-subsection:not(:last-child) {
border-bottom: 1px solid var(--color-interface-100);
}
.settings-subsection {
padding: var(--space-xs) 0;
h3 {
margin-top: calc(var(--space-xs)*1.5);
margin-bottom: calc(var(--space-xs)*2);
font-size: 1rem;
font-weight: 600;
}
}
}
.settings-subsection:not(:last-child) {
border-bottom: 1px solid var(--color-interface-100);
}
.settings-subsection {
padding: var(--space-xs) 0;
h3 {
margin-top: calc(var(--space-xs) * 1.5);
margin-bottom: calc(var(--space-xs) * 2);
font-size: 1rem;
font-weight: 600;
}
}

View file

@ -5,7 +5,9 @@ input[type="number"] {
border: 1px solid var(--color-interface-200);
background-color: var(--color-interface-100);
font-family: var(--sans-serif);
color: var(--color-txt);
font-size: 1rem;
padding-left: 0.5ch;
// min-width: var(--input-w);
// width: 100%;
// padding: 0 1ch;
@ -76,14 +78,27 @@ input[type="number"] {
width: 100%;
}
.field-checkbox{
grid-column: 2;
padding-top: var(--space-xs);
label{
font-weight: 400;
margin-left: 0.75ch;
color: var(--color-txt);
}
}
}
.field-text-size{
input[type="number"]{
width: var(--input-w-small);
padding-left: 0.75ch;
}
input[type="range"]{
flex-grow: 2;
flex-shrink: 2;
}
}
.field-margin, .field-size{
display: inline-grid;
width: calc(50% - 1ch);
@ -188,9 +203,13 @@ input[type="number"] {
top: 0;
button{
height: calc(var(--input-h)*0.6);
height: calc(var(--input-h)*0.5);
cursor: pointer;
padding: 0;
svg{
width: 10px;
height: auto;
}
svg path{
fill: var(--color-interface-600);
}
@ -202,8 +221,9 @@ input[type="number"] {
}
.spinner-down{
svg{
position: relative;
top: -2px;
// position: relative;
// top: -2px;
}
}

View file

@ -47,10 +47,7 @@
flex: 1;
padding: 1rem;
overflow-y: auto;
background: white;
display: flex;
flex-direction: column;
gap: 1rem;
background-color: var(--color-panel-bg);
}
.settings-subsection h4 {

View file

@ -9,7 +9,7 @@
--color-txt: var(--color-interface-800);
--color-txt: var(--color-interface-900);
--color-panel-bg: var(--color-interface-050);
--color-page-highlight: #ff8a50;
@ -20,6 +20,8 @@
--space-xs: 0.5rem;
--space-s: 1rem;
--space: 1.5rem;
--space-m: 2rem;
--space-big: 3em;
--curve: cubic-bezier(0.86, 0, 0.07, 1);
@ -34,4 +36,5 @@
font-size: 14px;
--panel-w: 540px;
--panel-nav-h: 60px;
}

View file

@ -223,7 +223,7 @@ img {
:root {
--color-browngray-050: #f5f3f0;
--color-browngray-200: #d0c4ba;
--color-txt: var(--color-interface-800);
--color-txt: var(--color-interface-900);
--color-panel-bg: var(--color-interface-050);
--color-page-highlight: #ff8a50;
--color-purple: #7136ff;
@ -231,6 +231,8 @@ img {
--space-xs: 0.5rem;
--space-s: 1rem;
--space: 1.5rem;
--space-m: 2rem;
--space-big: 3em;
--curve: cubic-bezier(0.86, 0, 0.07, 1);
--sans-serif: "DM Sans", sans-serif;
--mono: "Inconsolata", monospace;
@ -240,6 +242,7 @@ img {
--label-w: 18ch;
font-size: 14px;
--panel-w: 540px;
--panel-nav-h: 60px;
}
body {
@ -273,7 +276,9 @@ input[type=number] {
border: 1px solid var(--color-interface-200);
background-color: var(--color-interface-100);
font-family: var(--sans-serif);
color: var(--color-txt);
font-size: 1rem;
padding-left: 0.5ch;
}
.field {
@ -328,11 +333,22 @@ input[type=number] {
width: 100%;
}
.field-font .field-checkbox {
grid-column: 2;
padding-top: var(--space-xs);
}
.field-font .field-checkbox label {
font-weight: 400;
margin-left: 0.75ch;
color: var(--color-txt);
}
.field-text-size input[type=number] {
width: var(--input-w-small);
padding-left: 0.75ch;
}
.field-text-size input[type=range] {
flex-grow: 2;
flex-shrink: 2;
}
.field-margin, .field-size {
@ -421,23 +437,22 @@ input[type=number] {
top: 0;
}
.number-input .spinner-buttons button {
height: calc(var(--input-h) * 0.6);
height: calc(var(--input-h) * 0.5);
cursor: pointer;
padding: 0;
}
.number-input .spinner-buttons button svg {
width: 10px;
height: auto;
}
.number-input .spinner-buttons button svg path {
fill: var(--color-interface-600);
}
.number-input .spinner-buttons button:hover svg path {
fill: var(--color-interface-900);
}
.number-input .spinner-buttons .spinner-down svg {
position: relative;
top: -2px;
}
.settings-section {
margin-top: 3em;
margin: var(--space-m) 0;
}
.settings-section h2 {
margin-bottom: var(--space);
@ -450,13 +465,15 @@ input[type=number] {
font-size: 0.8rem;
color: var(--color-interface-400);
}
.settings-section .settings-subsection:not(:last-child) {
.settings-subsection:not(:last-child) {
border-bottom: 1px solid var(--color-interface-100);
}
.settings-section .settings-subsection {
.settings-subsection {
padding: var(--space-xs) 0;
}
.settings-section .settings-subsection h3 {
.settings-subsection h3 {
margin-top: calc(var(--space-xs) * 1.5);
margin-bottom: calc(var(--space-xs) * 2);
font-size: 1rem;
@ -549,10 +566,7 @@ input[type=number] {
flex: 1;
padding: 1rem;
overflow-y: auto;
background: white;
display: flex;
flex-direction: column;
gap: 1rem;
background-color: var(--color-panel-bg);
}
.settings-subsection h4 {

File diff suppressed because one or more lines are too long

View file

@ -16,7 +16,7 @@ return array(
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'League\\ColorExtractor\\' => array($vendorDir . '/league/color-extractor/src'),
'Laminas\\Escaper\\' => array($vendorDir . '/laminas/laminas-escaper/src'),
'Kirby\\' => array($vendorDir . '/getkirby/composer-installer/src', $baseDir . '/kirby/src'),
'Kirby\\' => array($baseDir . '/kirby/src', $vendorDir . '/getkirby/composer-installer/src'),
'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
'Base32\\' => array($vendorDir . '/christian-riesen/base32/src'),
);

View file

@ -96,8 +96,8 @@ class ComposerStaticInit0b7fb803e22a45eb87e24172337208aa
),
'Kirby\\' =>
array (
0 => __DIR__ . '/..' . '/getkirby/composer-installer/src',
1 => __DIR__ . '/../..' . '/kirby/src',
0 => __DIR__ . '/../..' . '/kirby/src',
1 => __DIR__ . '/..' . '/getkirby/composer-installer/src',
),
'Composer\\Semver\\' =>
array (

View file

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'getkirby/plainkit',
'pretty_version' => '5.1.4',
'version' => '5.1.4.0',
'reference' => null,
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '76274fff04c54514230ad2bb0aca362139618411',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -65,9 +65,9 @@
'dev_requirement' => false,
),
'getkirby/plainkit' => array(
'pretty_version' => '5.1.4',
'version' => '5.1.4.0',
'reference' => null,
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '76274fff04c54514230ad2bb0aca362139618411',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View file

@ -18,19 +18,17 @@
<div class="popup-controls">
<!-- Font Family -->
<div class="settings-subsection">
<div class="field" :class="{ 'field--view-only': inheritanceLocked }">
<div class="field field-font" :class="{ 'field--view-only': inheritanceLocked }">
<label class="label-with-tooltip" data-css="font-family">Police</label>
<select v-model="fontFamily.value" :disabled="inheritanceLocked">
<option value="Alegreya Sans">Alegreya Sans</option>
<option value="Alegreya">Alegreya</option>
<option value="Arial">Arial</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">Times New Roman</option>
</select>
<label class="checkbox-inline label-with-tooltip" data-css="font-style">
<input type="checkbox" v-model="fontStyle.italic" :disabled="inheritanceLocked" />
<span>Italique</span>
</label>
<div class="field-with-option">
<select v-model="fontFamily.value" :disabled="inheritanceLocked">
<option v-for="f in fonts" :key="f" :value="f">{{ f }}</option>
</select>
<div class="field-checkbox">
<input type="checkbox" v-model="fontStyle.italic" :disabled="inheritanceLocked" />
<label class="label-with-tooltip" data-css="font-style">Italique</label>
</div>
</div>
</div>
</div>
@ -38,18 +36,7 @@
<div class="settings-subsection">
<div class="field" :class="{ 'field--view-only': inheritanceLocked }">
<label class="label-with-tooltip" data-css="font-weight">Graisse</label>
<div class="button-group">
<button
v-for="weight in [200, 300, 400, 600, 800]"
:key="weight"
type="button"
:class="{ active: fontWeight.value === weight }"
:disabled="inheritanceLocked"
@click="fontWeight.value = weight"
>
{{ weight }}
</button>
</div>
<UnitToggle v-model="fontWeightString" :units="weights" :disabled="inheritanceLocked" />
</div>
</div>
@ -276,6 +263,7 @@ import { useStylesheetStore } from '../stores/stylesheet';
import { usePopupPosition } from '../composables/usePopupPosition';
import { useDebounce } from '../composables/useDebounce';
import NumberInput from './ui/NumberInput.vue';
import UnitToggle from './ui/UnitToggle.vue';
import Coloris from '@melloware/coloris';
import '@melloware/coloris/dist/coloris.css';
import hljs from 'highlight.js/lib/core';
@ -321,6 +309,18 @@ const background = ref({ value: 'transparent' });
const marginOuter = ref({ value: 0, unit: 'mm' });
const paddingInner = ref({ value: 0, unit: 'mm' });
// Constants
const fonts = ['Alegreya Sans', 'Alegreya', 'Arial', 'Georgia', 'Times New Roman'];
const weights = ['200', '300', '400', '600', '800'];
// Computed to adapt fontWeight for UnitToggle
const fontWeightString = computed({
get: () => String(fontWeight.value.value),
set: (val) => {
fontWeight.value.value = parseInt(val);
}
});
const immediateUpdate = (callback) => {
callback();
};
@ -794,43 +794,4 @@ defineExpose({ handleIframeClick, close, visible });
color: var(--color-purple);
font-size: 0.875rem;
}
.button-group {
display: flex;
gap: 0.25rem;
}
.button-group button {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.button-group button.active {
background: #61afef;
color: white;
border-color: #61afef;
}
.button-group button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-inline {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: 1rem;
font-size: 0.875rem;
cursor: pointer;
}
.checkbox-inline input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
</style>

View file

@ -128,11 +128,11 @@ nav {
position: relative;
left: calc(var(--panel-w) * -1);
padding: 4rem 0;
background-color: var(--color-panel-bg);
box-shadow: -5px 0px 12px;
transition: left 0.3s var(--curve);
pointer-events: all;
}
@ -142,10 +142,10 @@ nav {
}
.tab-panel {
height: 100%;
height: calc(100% - var(--panel-nav-h)*2);
overflow-y: auto;
overflow-x: hidden;
padding: 0 2em;
// padding-left: 1em;
margin-top: var(--panel-nav-h);
}
</style>

View file

@ -34,7 +34,7 @@
<!-- Taille du texte -->
<div class="settings-subsection">
<div class="field">
<div class="field field-text-size">
<label for="text-size-range" class="label-with-tooltip" data-css="font-size">Taille du texte</label>
<InputWithUnit
v-model="fontSize"
@ -91,31 +91,269 @@
</div>
<!-- Marges extérieures -->
<div class="settings-subsection">
<MarginEditor
ref="marginOuterEditor"
id="margin-outer"
label="Marges extérieures"
cssProperty="margin"
v-model:simple="marginOuter"
v-model:detailed="marginOuterDetailed"
:units="['mm', 'px', 'rem']"
@change="handleMarginOuterChange"
/>
<div class="settings-subsection margins">
<div class="subsection-header">
<h3>Marges extérieures</h3>
<button
type="button"
class="link-button"
:class="{ active: marginOuterLinked }"
@click="marginOuterLinked = !marginOuterLinked"
:title="marginOuterLinked ? 'Dissocier les marges' : 'Lier les marges'"
>
<svg v-if="marginOuterLinked" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.3638 15.5355L16.9496 14.1213L18.3638 12.7071C20.3164 10.7545 20.3164 7.58866 18.3638 5.63604C16.4112 3.68341 13.2453 3.68341 11.2927 5.63604L9.87849 7.05025L8.46428 5.63604L9.87849 4.22182C12.6122 1.48815 17.0443 1.48815 19.778 4.22182C22.5117 6.95549 22.5117 11.3876 19.778 14.1213L18.3638 15.5355ZM15.5353 18.364L14.1211 19.7782C11.3875 22.5118 6.95531 22.5118 4.22164 19.7782C1.48797 17.0445 1.48797 12.6123 4.22164 9.87868L5.63585 8.46446L7.05007 9.87868L5.63585 11.2929C3.68323 13.2455 3.68323 16.4113 5.63585 18.364C7.58847 20.3166 10.7543 20.3166 12.7069 18.364L14.1211 16.9497L15.5353 18.364ZM14.8282 7.75736L16.2425 9.17157L9.17139 16.2426L7.75717 14.8284L14.8282 7.75736Z"></path></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 17H22V19H19V22H17V17ZM7 7H2V5H5V2H7V7ZM18.364 15.5355L16.9497 14.1213L18.364 12.7071C20.3166 10.7545 20.3166 7.58866 18.364 5.63604C16.4113 3.68342 13.2455 3.68342 11.2929 5.63604L9.87868 7.05025L8.46447 5.63604L9.87868 4.22183C12.6123 1.48816 17.0445 1.48816 19.7782 4.22183C22.5118 6.9555 22.5118 11.3877 19.7782 14.1213L18.364 15.5355ZM15.5355 18.364L14.1213 19.7782C11.3877 22.5118 6.9555 22.5118 4.22183 19.7782C1.48816 17.0445 1.48816 12.6123 4.22183 9.87868L5.63604 8.46447L7.05025 9.87868L5.63604 11.2929C3.68342 13.2455 3.68342 16.4113 5.63604 18.364C7.58866 20.3166 10.7545 20.3166 12.7071 18.364L14.1213 16.9497L15.5355 18.364ZM14.8284 7.75736L16.2426 9.17157L9.17157 16.2426L7.75736 14.8284L14.8284 7.75736Z"></path></svg>
</button>
</div>
<div class="field field-margin">
<label for="margin-outer-top" class="label-with-tooltip" data-css="margin-top">Haut</label>
<div class="input-with-unit">
<NumberInput
id="margin-outer-top"
v-model="marginOuterDetailed.top.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginOuterDetailed.top.unit === 'mm' }"
@click="updateMarginOuterUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginOuterDetailed.top.unit === 'px' }"
@click="updateMarginOuterUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginOuterDetailed.top.unit === 'rem' }"
@click="updateMarginOuterUnit('rem')"
>rem</button>
</div>
</div>
</div>
<div class="field field-margin">
<label for="margin-outer-bottom" class="label-with-tooltip" data-css="margin-bottom">Bas</label>
<div class="input-with-unit">
<NumberInput
id="margin-outer-bottom"
v-model="marginOuterDetailed.bottom.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginOuterDetailed.bottom.unit === 'mm' }"
@click="updateMarginOuterUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginOuterDetailed.bottom.unit === 'px' }"
@click="updateMarginOuterUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginOuterDetailed.bottom.unit === 'rem' }"
@click="updateMarginOuterUnit('rem')"
>rem</button>
</div>
</div>
</div>
<div class="field field-margin">
<label for="margin-outer-left" class="label-with-tooltip" data-css="margin-left">Gauche</label>
<div class="input-with-unit">
<NumberInput
id="margin-outer-left"
v-model="marginOuterDetailed.left.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginOuterDetailed.left.unit === 'mm' }"
@click="updateMarginOuterUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginOuterDetailed.left.unit === 'px' }"
@click="updateMarginOuterUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginOuterDetailed.left.unit === 'rem' }"
@click="updateMarginOuterUnit('rem')"
>rem</button>
</div>
</div>
</div>
<div class="field field-margin">
<label for="margin-outer-right" class="label-with-tooltip" data-css="margin-right">Droite</label>
<div class="input-with-unit">
<NumberInput
id="margin-outer-right"
v-model="marginOuterDetailed.right.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginOuterDetailed.right.unit === 'mm' }"
@click="updateMarginOuterUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginOuterDetailed.right.unit === 'px' }"
@click="updateMarginOuterUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginOuterDetailed.right.unit === 'rem' }"
@click="updateMarginOuterUnit('rem')"
>rem</button>
</div>
</div>
</div>
</div>
<!-- Marges intérieures -->
<div class="settings-subsection">
<MarginEditor
ref="marginInnerEditor"
id="margin-inner"
label="Marges intérieures"
cssProperty="padding"
v-model:simple="marginInner"
v-model:detailed="marginInnerDetailed"
:units="['mm', 'px', 'rem']"
@change="handleMarginInnerChange"
/>
<div class="settings-subsection margins">
<div class="subsection-header">
<h3>Marges intérieures</h3>
<button
type="button"
class="link-button"
:class="{ active: marginInnerLinked }"
@click="marginInnerLinked = !marginInnerLinked"
:title="marginInnerLinked ? 'Dissocier les marges' : 'Lier les marges'"
>
<svg v-if="marginInnerLinked" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.3638 15.5355L16.9496 14.1213L18.3638 12.7071C20.3164 10.7545 20.3164 7.58866 18.3638 5.63604C16.4112 3.68341 13.2453 3.68341 11.2927 5.63604L9.87849 7.05025L8.46428 5.63604L9.87849 4.22182C12.6122 1.48815 17.0443 1.48815 19.778 4.22182C22.5117 6.95549 22.5117 11.3876 19.778 14.1213L18.3638 15.5355ZM15.5353 18.364L14.1211 19.7782C11.3875 22.5118 6.95531 22.5118 4.22164 19.7782C1.48797 17.0445 1.48797 12.6123 4.22164 9.87868L5.63585 8.46446L7.05007 9.87868L5.63585 11.2929C3.68323 13.2455 3.68323 16.4113 5.63585 18.364C7.58847 20.3166 10.7543 20.3166 12.7069 18.364L14.1211 16.9497L15.5353 18.364ZM14.8282 7.75736L16.2425 9.17157L9.17139 16.2426L7.75717 14.8284L14.8282 7.75736Z"></path></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 17H22V19H19V22H17V17ZM7 7H2V5H5V2H7V7ZM18.364 15.5355L16.9497 14.1213L18.364 12.7071C20.3166 10.7545 20.3166 7.58866 18.364 5.63604C16.4113 3.68342 13.2455 3.68342 11.2929 5.63604L9.87868 7.05025L8.46447 5.63604L9.87868 4.22183C12.6123 1.48816 17.0445 1.48816 19.7782 4.22183C22.5118 6.9555 22.5118 11.3877 19.7782 14.1213L18.364 15.5355ZM15.5355 18.364L14.1213 19.7782C11.3877 22.5118 6.9555 22.5118 4.22183 19.7782C1.48816 17.0445 1.48816 12.6123 4.22183 9.87868L5.63604 8.46447L7.05025 9.87868L5.63604 11.2929C3.68342 13.2455 3.68342 16.4113 5.63604 18.364C7.58866 20.3166 10.7545 20.3166 12.7071 18.364L14.1213 16.9497L15.5355 18.364ZM14.8284 7.75736L16.2426 9.17157L9.17157 16.2426L7.75736 14.8284L14.8284 7.75736Z"></path></svg>
</button>
</div>
<div class="field field-margin">
<label for="margin-inner-top" class="label-with-tooltip" data-css="padding-top">Haut</label>
<div class="input-with-unit">
<NumberInput
id="margin-inner-top"
v-model="marginInnerDetailed.top.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginInnerDetailed.top.unit === 'mm' }"
@click="updateMarginInnerUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginInnerDetailed.top.unit === 'px' }"
@click="updateMarginInnerUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginInnerDetailed.top.unit === 'rem' }"
@click="updateMarginInnerUnit('rem')"
>rem</button>
</div>
</div>
</div>
<div class="field field-margin">
<label for="margin-inner-bottom" class="label-with-tooltip" data-css="padding-bottom">Bas</label>
<div class="input-with-unit">
<NumberInput
id="margin-inner-bottom"
v-model="marginInnerDetailed.bottom.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginInnerDetailed.bottom.unit === 'mm' }"
@click="updateMarginInnerUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginInnerDetailed.bottom.unit === 'px' }"
@click="updateMarginInnerUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginInnerDetailed.bottom.unit === 'rem' }"
@click="updateMarginInnerUnit('rem')"
>rem</button>
</div>
</div>
</div>
<div class="field field-margin">
<label for="margin-inner-left" class="label-with-tooltip" data-css="padding-left">Gauche</label>
<div class="input-with-unit">
<NumberInput
id="margin-inner-left"
v-model="marginInnerDetailed.left.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginInnerDetailed.left.unit === 'mm' }"
@click="updateMarginInnerUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginInnerDetailed.left.unit === 'px' }"
@click="updateMarginInnerUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginInnerDetailed.left.unit === 'rem' }"
@click="updateMarginInnerUnit('rem')"
>rem</button>
</div>
</div>
</div>
<div class="field field-margin">
<label for="margin-inner-right" class="label-with-tooltip" data-css="padding-right">Droite</label>
<div class="input-with-unit">
<NumberInput
id="margin-inner-right"
v-model="marginInnerDetailed.right.value"
:min="0"
:step="1"
/>
<div class="unit-toggle">
<button
type="button"
:class="{ active: marginInnerDetailed.right.unit === 'mm' }"
@click="updateMarginInnerUnit('mm')"
>mm</button>
<button
type="button"
:class="{ active: marginInnerDetailed.right.unit === 'px' }"
@click="updateMarginInnerUnit('px')"
>px</button>
<button
type="button"
:class="{ active: marginInnerDetailed.right.unit === 'rem' }"
@click="updateMarginInnerUnit('rem')"
>rem</button>
</div>
</div>
</div>
</div>
</div>
@ -127,7 +365,7 @@ import { ref, watch, onMounted } from 'vue';
import Coloris from '@melloware/coloris';
import UnitToggle from '../ui/UnitToggle.vue';
import InputWithUnit from '../ui/InputWithUnit.vue';
import MarginEditor from '../ui/MarginEditor.vue';
import NumberInput from '../ui/NumberInput.vue';
import { useCssUpdater } from '../../composables/useCssUpdater';
import { useCssSync } from '../../composables/useCssSync';
import { useDebounce } from '../../composables/useDebounce';
@ -155,7 +393,6 @@ const alignment = ref('left');
const color = ref('rgb(0, 0, 0)');
const background = ref('transparent');
const marginOuter = ref({ value: 0, unit: 'mm' });
const marginOuterDetailed = ref({
top: { value: 0, unit: 'mm' },
right: { value: 0, unit: 'mm' },
@ -163,7 +400,6 @@ const marginOuterDetailed = ref({
left: { value: 0, unit: 'mm' }
});
const marginInner = ref({ value: 0, unit: 'mm' });
const marginInnerDetailed = ref({
top: { value: 0, unit: 'mm' },
right: { value: 0, unit: 'mm' },
@ -171,11 +407,27 @@ const marginInnerDetailed = ref({
left: { value: 0, unit: 'mm' }
});
const marginOuterEditor = ref(null);
const marginInnerEditor = ref(null);
const marginOuterLinked = ref(false);
const marginInnerLinked = ref(false);
let isUpdatingFromStore = false;
// Update margin outer unit for all sides
const updateMarginOuterUnit = (unit) => {
marginOuterDetailed.value.top.unit = unit;
marginOuterDetailed.value.right.unit = unit;
marginOuterDetailed.value.bottom.unit = unit;
marginOuterDetailed.value.left.unit = unit;
};
// Update margin inner unit for all sides
const updateMarginInnerUnit = (unit) => {
marginInnerDetailed.value.top.unit = unit;
marginInnerDetailed.value.right.unit = unit;
marginInnerDetailed.value.bottom.unit = unit;
marginInnerDetailed.value.left.unit = unit;
};
// Watchers for body styles
watch(italic, (val) => {
if (isUpdatingFromStore) return;
@ -210,28 +462,77 @@ watch(fontSize, (val) => {
});
}, { deep: true });
// Margin/Padding handlers
const handleMarginOuterChange = ({ type, simple, detailed }) => {
// Watch margin outer values
watch(() => [
marginOuterDetailed.value.top.value,
marginOuterDetailed.value.bottom.value,
marginOuterDetailed.value.left.value,
marginOuterDetailed.value.right.value,
], () => {
if (isUpdatingFromStore) return;
debouncedUpdate(() => {
if (type === 'simple') {
setMargin('p', simple.value, simple.unit);
} else {
setDetailedMargins('p', detailed.top, detailed.right, detailed.bottom, detailed.left);
}
});
};
const handleMarginInnerChange = ({ type, simple, detailed }) => {
if (isUpdatingFromStore) return;
// If linked, sync all values to the one that changed
if (marginOuterLinked.value) {
// Find which value changed and sync others to it
const top = marginOuterDetailed.value.top.value;
const bottom = marginOuterDetailed.value.bottom.value;
const left = marginOuterDetailed.value.left.value;
const right = marginOuterDetailed.value.right.value;
// Use the max value to determine which one changed (simple heuristic)
const maxValue = Math.max(top, bottom, left, right);
isUpdatingFromStore = true;
marginOuterDetailed.value.top.value = maxValue;
marginOuterDetailed.value.bottom.value = maxValue;
marginOuterDetailed.value.left.value = maxValue;
marginOuterDetailed.value.right.value = maxValue;
isUpdatingFromStore = false;
}
debouncedUpdate(() => {
if (type === 'simple') {
setPadding('p', simple.value, simple.unit);
} else {
setDetailedPadding('p', detailed.top, detailed.right, detailed.bottom, detailed.left);
}
setDetailedMargins('p',
marginOuterDetailed.value.top,
marginOuterDetailed.value.right,
marginOuterDetailed.value.bottom,
marginOuterDetailed.value.left
);
});
};
});
// Watch margin inner values
watch(() => [
marginInnerDetailed.value.top.value,
marginInnerDetailed.value.bottom.value,
marginInnerDetailed.value.left.value,
marginInnerDetailed.value.right.value,
], () => {
if (isUpdatingFromStore) return;
// If linked, sync all values to the one that changed
if (marginInnerLinked.value) {
const top = marginInnerDetailed.value.top.value;
const bottom = marginInnerDetailed.value.bottom.value;
const left = marginInnerDetailed.value.left.value;
const right = marginInnerDetailed.value.right.value;
const maxValue = Math.max(top, bottom, left, right);
isUpdatingFromStore = true;
marginInnerDetailed.value.top.value = maxValue;
marginInnerDetailed.value.bottom.value = maxValue;
marginInnerDetailed.value.left.value = maxValue;
marginInnerDetailed.value.right.value = maxValue;
isUpdatingFromStore = false;
}
debouncedUpdate(() => {
setDetailedPadding('p',
marginInnerDetailed.value.top,
marginInnerDetailed.value.right,
marginInnerDetailed.value.bottom,
marginInnerDetailed.value.left
);
});
});
// Sync from store
const syncFromStore = () => {
@ -261,17 +562,17 @@ const syncFromStore = () => {
const margins = extractSpacing('p', 'margin');
if (margins) {
if (margins.simple) {
marginOuter.value = margins.simple;
// Sync detailed from simple
// All margins are the same
marginOuterDetailed.value = {
top: { ...margins.simple },
right: { ...margins.simple },
bottom: { ...margins.simple },
left: { ...margins.simple }
};
marginOuterLinked.value = true;
} else if (margins.detailed) {
marginOuterDetailed.value = margins.detailed;
// Check if all values are the same to set simple value
// Check if all values are the same
const allSame =
margins.detailed.top.value === margins.detailed.right.value &&
margins.detailed.top.value === margins.detailed.bottom.value &&
@ -279,19 +580,7 @@ const syncFromStore = () => {
margins.detailed.top.unit === margins.detailed.right.unit &&
margins.detailed.top.unit === margins.detailed.bottom.unit &&
margins.detailed.top.unit === margins.detailed.left.unit;
if (allSame) {
marginOuter.value = margins.detailed.top;
} else {
// Values are different, open the detailed editor and use first value for simple
marginOuter.value = margins.detailed.top;
// Open detailed view after mount
setTimeout(() => {
if (marginOuterEditor.value) {
marginOuterEditor.value.expanded = true;
}
}, 0);
}
marginOuterLinked.value = allSame;
}
}
@ -299,17 +588,17 @@ const syncFromStore = () => {
const padding = extractSpacing('p', 'padding');
if (padding) {
if (padding.simple) {
marginInner.value = padding.simple;
// Sync detailed from simple
// All paddings are the same
marginInnerDetailed.value = {
top: { ...padding.simple },
right: { ...padding.simple },
bottom: { ...padding.simple },
left: { ...padding.simple }
};
marginInnerLinked.value = true;
} else if (padding.detailed) {
marginInnerDetailed.value = padding.detailed;
// Check if all values are the same to set simple value
// Check if all values are the same
const allSame =
padding.detailed.top.value === padding.detailed.right.value &&
padding.detailed.top.value === padding.detailed.bottom.value &&
@ -317,19 +606,7 @@ const syncFromStore = () => {
padding.detailed.top.unit === padding.detailed.right.unit &&
padding.detailed.top.unit === padding.detailed.bottom.unit &&
padding.detailed.top.unit === padding.detailed.left.unit;
if (allSame) {
marginInner.value = padding.detailed.top;
} else {
// Values are different, open the detailed editor and use first value for simple
marginInner.value = padding.detailed.top;
// Open detailed view after mount
setTimeout(() => {
if (marginInnerEditor.value) {
marginInnerEditor.value.expanded = true;
}
}, 0);
}
marginInnerLinked.value = allSame;
}
}
@ -348,3 +625,49 @@ onMounted(() => {
syncFromStore();
});
</script>
<style scoped>
.subsection-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.subsection-header h3 {
margin: 0;
}
.link-button {
background: none;
border: 1px solid var(--color-border, #ddd);
border-radius: 4px;
padding: 0.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
width: 24px;
height: 24px;
}
.link-button svg {
width: 16px;
height: 16px;
color: var(--color-text-secondary, #666);
}
.link-button:hover {
background: var(--color-hover, #f0f0f0);
}
.link-button.active {
background: var(--color-primary, #007bff);
border-color: var(--color-primary, #007bff);
}
.link-button.active svg {
color: white;
}
</style>

View file

@ -5,6 +5,7 @@
:key="unit"
type="button"
:class="{ active: modelValue === unit }"
:disabled="disabled"
@click="$emit('update:modelValue', unit)"
>
{{ unit }}
@ -21,6 +22,10 @@ defineProps({
units: {
type: Array,
default: () => ['mm', 'px']
},
disabled: {
type: Boolean,
default: false
}
});

View file

@ -4,4 +4,23 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
publicDir: false, // Les assets statiques sont gérés par Kirby dans public/
build: {
outDir: 'dist/assets/dist',
emptyOutDir: true,
manifest: false,
rollupOptions: {
output: {
entryFileNames: 'index.js',
chunkFileNames: '[name].js',
assetFileNames: (assetInfo) => {
// Le CSS principal doit s'appeler index.css pour correspondre à header.php
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
return 'index.css'
}
return '[name].[ext]'
}
}
}
}
})