Compare commits

...

7 commits

Author SHA1 Message Date
Julie Blanc
d88758b226 filed font-size 2025-12-10 13:29:14 +01:00
isUnknown
76274fff04 feat: apply field values when unlocking inheritance
- Unlocking now creates CSS block with current field values
- Locking removes CSS block to restore general styles
- Show commented CSS preview when inheritance is locked
- Preview displays what would be applied if unlocked

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 12:11:53 +01:00
isUnknown
d9f3ede661 feat: persist inheritance lock state per element via data attribute
- Store unlock state in data-inheritance-unlocked attribute on DOM element
- Each element/page now remembers its own inheritance state
- Re-locking removes element-specific CSS block to restore inheritance
- Elements revert to general styles from TextSettings/PageSettings

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 12:04:58 +01:00
isUnknown
668d950518 fix: auto-create CSS rules when selector or property is missing
updateCssValue now handles three cases:
- Selector doesn't exist: creates the full block with property
- Selector exists but property missing: adds property to block
- Property exists: updates the value

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 11:59:09 +01:00
isUnknown
681517db21 refactor: extract debounce logic into shared composable
- Create useDebounce composable to avoid code duplication
- Apply debounce to TextSettings margin/padding inputs
- Harmonize debounce delay to 500ms across all components
- Fix input lag when typing values like "30mm"

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 11:51:53 +01:00
isUnknown
35c9ab1d3b merge 2025-12-10 11:35:59 +01:00
isUnknown
ceaf318272 improve default styles 2025-12-10 11:35:37 +01:00
17 changed files with 320 additions and 96 deletions

View file

@ -1,10 +1,8 @@
.settings-section { .settings-section {
margin-top: 3em; margin: var(--space-m) 0;
// .cons
h2 { h2 {
@ -12,21 +10,10 @@
font-weight: 600; font-weight: 600;
font-size: 1.4rem; font-size: 1.4rem;
// border-radius: var(--border-radius);
// height: var(--input-h);
// padding: 0 1ch;
// display: flex;
// align-items: center;
// color: var(--color-interface-050);
// background-color: var(--color-800);
// display: inline-block;
border-bottom: 1px solid var(--color-200); border-bottom: 1px solid var(--color-200);
color: var(--color-800); color: var(--color-800);
// border-bottom: 2px solid currentColor;
// margin-bottom: 2rem;
} }
.infos{ .infos{

View file

@ -5,7 +5,9 @@ input[type="number"] {
border: 1px solid var(--color-interface-200); border: 1px solid var(--color-interface-200);
background-color: var(--color-interface-100); background-color: var(--color-interface-100);
font-family: var(--sans-serif); font-family: var(--sans-serif);
color: var(--color-txt);
font-size: 1rem; font-size: 1rem;
padding-left: 0.5ch;
// min-width: var(--input-w); // min-width: var(--input-w);
// width: 100%; // width: 100%;
// padding: 0 1ch; // padding: 0 1ch;
@ -85,10 +87,22 @@ input[type="number"] {
label{ label{
font-weight: 400; font-weight: 400;
margin-left: 0.75ch; 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{ .field-margin, .field-size{
display: inline-grid; display: inline-grid;
width: calc(50% - 1ch); width: calc(50% - 1ch);
@ -196,9 +210,13 @@ input[type="number"] {
top: 0; top: 0;
button{ button{
height: calc(var(--input-h)*0.6); height: calc(var(--input-h)*0.5);
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
svg{
width: 10px;
height: auto;
}
svg path{ svg path{
fill: var(--color-interface-600); fill: var(--color-interface-600);
} }
@ -210,8 +228,9 @@ input[type="number"] {
} }
.spinner-down{ .spinner-down{
svg{ svg{
position: relative;
top: -2px; // position: relative;
// top: -2px;
} }
} }

View file

@ -25,3 +25,7 @@ button {
background-color: transparent; background-color: transparent;
border: none; border: none;
} }
img {
width: 100%;
}

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

View file

@ -216,10 +216,14 @@ button {
border: none; border: none;
} }
img {
width: 100%;
}
:root { :root {
--color-browngray-050: #f5f3f0; --color-browngray-050: #f5f3f0;
--color-browngray-200: #d0c4ba; --color-browngray-200: #d0c4ba;
--color-txt: var(--color-interface-800); --color-txt: var(--color-interface-900);
--color-panel-bg: var(--color-interface-050); --color-panel-bg: var(--color-interface-050);
--color-page-highlight: #ff8a50; --color-page-highlight: #ff8a50;
--color-purple: #7136ff; --color-purple: #7136ff;
@ -227,6 +231,8 @@ button {
--space-xs: 0.5rem; --space-xs: 0.5rem;
--space-s: 1rem; --space-s: 1rem;
--space: 1.5rem; --space: 1.5rem;
--space-m: 2rem;
--space-big: 3em;
--curve: cubic-bezier(0.86, 0, 0.07, 1); --curve: cubic-bezier(0.86, 0, 0.07, 1);
--sans-serif: "DM Sans", sans-serif; --sans-serif: "DM Sans", sans-serif;
--mono: "Inconsolata", monospace; --mono: "Inconsolata", monospace;
@ -236,6 +242,7 @@ button {
--label-w: 18ch; --label-w: 18ch;
font-size: 14px; font-size: 14px;
--panel-w: 540px; --panel-w: 540px;
--panel-nav-h: 60px;
} }
body { body {
@ -269,7 +276,9 @@ input[type=number] {
border: 1px solid var(--color-interface-200); border: 1px solid var(--color-interface-200);
background-color: var(--color-interface-100); background-color: var(--color-interface-100);
font-family: var(--sans-serif); font-family: var(--sans-serif);
color: var(--color-txt);
font-size: 1rem; font-size: 1rem;
padding-left: 0.5ch;
} }
.field { .field {
@ -329,6 +338,16 @@ input[type=number] {
.field-font .field-checkbox label { .field-font .field-checkbox label {
font-weight: 400; font-weight: 400;
margin-left: 0.75ch; 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 { .field-margin, .field-size {
@ -417,23 +436,22 @@ input[type=number] {
top: 0; top: 0;
} }
.number-input .spinner-buttons button { .number-input .spinner-buttons button {
height: calc(var(--input-h) * 0.6); height: calc(var(--input-h) * 0.5);
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
} }
.number-input .spinner-buttons button svg {
width: 10px;
height: auto;
}
.number-input .spinner-buttons button svg path { .number-input .spinner-buttons button svg path {
fill: var(--color-interface-600); fill: var(--color-interface-600);
} }
.number-input .spinner-buttons button:hover svg path { .number-input .spinner-buttons button:hover svg path {
fill: var(--color-interface-900); fill: var(--color-interface-900);
} }
.number-input .spinner-buttons .spinner-down svg {
position: relative;
top: -2px;
}
.settings-section { .settings-section {
margin-top: 3em; margin: var(--space-m) 0;
} }
.settings-section h2 { .settings-section h2 {
margin-bottom: var(--space); margin-bottom: var(--space);

File diff suppressed because one or more lines are too long

View file

@ -24,5 +24,13 @@ h2 {
p { p {
font-size: 1rem; font-size: 1rem;
margin: 0mm 0mm 24mm 0mm; margin: 0mm 0mm 10mm 0mm;
}
img {
width: 100%;
}
li p {
margin-bottom: 0;
} }

View file

@ -16,7 +16,7 @@ return array(
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'), 'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'League\\ColorExtractor\\' => array($vendorDir . '/league/color-extractor/src'), 'League\\ColorExtractor\\' => array($vendorDir . '/league/color-extractor/src'),
'Laminas\\Escaper\\' => array($vendorDir . '/laminas/laminas-escaper/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'), 'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
'Base32\\' => array($vendorDir . '/christian-riesen/base32/src'), 'Base32\\' => array($vendorDir . '/christian-riesen/base32/src'),
); );

View file

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

View file

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

View file

@ -274,6 +274,7 @@
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet'; import { useStylesheetStore } from '../stores/stylesheet';
import { usePopupPosition } from '../composables/usePopupPosition'; import { usePopupPosition } from '../composables/usePopupPosition';
import { useDebounce } from '../composables/useDebounce';
import NumberInput from './ui/NumberInput.vue'; import NumberInput from './ui/NumberInput.vue';
import Coloris from '@melloware/coloris'; import Coloris from '@melloware/coloris';
import '@melloware/coloris/dist/coloris.css'; import '@melloware/coloris/dist/coloris.css';
@ -307,7 +308,7 @@ const colorInput = ref(null);
const backgroundInput = ref(null); const backgroundInput = ref(null);
let isUpdatingFromStore = false; let isUpdatingFromStore = false;
let updateTimer = null; const { debouncedUpdate } = useDebounce(500);
// Style properties // Style properties
const fontFamily = ref({ value: 'Alegreya Sans' }); const fontFamily = ref({ value: 'Alegreya Sans' });
@ -320,11 +321,6 @@ const background = ref({ value: 'transparent' });
const marginOuter = ref({ value: 0, unit: 'mm' }); const marginOuter = ref({ value: 0, unit: 'mm' });
const paddingInner = ref({ value: 0, unit: 'mm' }); const paddingInner = ref({ value: 0, unit: 'mm' });
const debouncedUpdate = (callback) => {
clearTimeout(updateTimer);
updateTimer = setTimeout(callback, 1000);
};
const immediateUpdate = (callback) => { const immediateUpdate = (callback) => {
callback(); callback();
}; };
@ -373,9 +369,81 @@ const elementCss = computed(() => {
return stylesheetStore.extractBlock(selector.value) || ''; return stylesheetStore.extractBlock(selector.value) || '';
}); });
// Generate a preview CSS block from current field values
const generatePreviewCss = () => {
if (!selector.value) return '';
const properties = [];
if (fontFamily.value.value) {
properties.push(` font-family: ${fontFamily.value.value};`);
}
if (fontStyle.value.italic) {
properties.push(` font-style: italic;`);
}
if (fontWeight.value.value) {
properties.push(` font-weight: ${fontWeight.value.value};`);
}
if (fontSize.value.value) {
properties.push(` font-size: ${fontSize.value.value}${fontSize.value.unit};`);
}
if (textAlign.value.value) {
properties.push(` text-align: ${textAlign.value.value};`);
}
if (color.value.value && color.value.value !== 'rgb(0, 0, 0)') {
properties.push(` color: ${color.value.value};`);
}
if (background.value.value && background.value.value !== 'transparent') {
properties.push(` background: ${background.value.value};`);
}
if (marginOuter.value.value) {
properties.push(` margin: ${marginOuter.value.value}${marginOuter.value.unit};`);
}
if (paddingInner.value.value) {
properties.push(` padding: ${paddingInner.value.value}${paddingInner.value.unit};`);
}
if (properties.length === 0) return '';
return `${selector.value} {\n${properties.join('\n')}\n}`;
};
const displayedCss = computed(() => {
if (!selector.value) return '';
// If unlocked, show the actual CSS block from stylesheet
if (!inheritanceLocked.value) {
return elementCss.value || generatePreviewCss();
}
// If locked, show commented preview of what would be applied
const preview = generatePreviewCss();
if (!preview) return '';
return '/* Héritage verrouillé - déverrouiller pour appliquer */\n/* ' +
preview.split('\n').join('\n ') +
' */';
});
// Remove the element-specific CSS block to restore inheritance
const removeElementBlock = () => {
if (!selector.value) return;
const block = stylesheetStore.extractBlock(selector.value);
if (block) {
// Escape special regex characters in selector
const escaped = selector.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Remove the block and any surrounding whitespace
stylesheetStore.content = stylesheetStore.content.replace(
new RegExp(`\\n?${escaped}\\s*\\{[^}]*\\}\\n?`),
'\n'
);
}
};
const highlightedCss = computed(() => { const highlightedCss = computed(() => {
if (!elementCss.value) return ''; if (!displayedCss.value) return '';
return hljs.highlight(elementCss.value, { language: 'css' }).value; return hljs.highlight(displayedCss.value, { language: 'css' }).value;
}); });
// Update functions for each property // Update functions for each property
@ -424,6 +492,21 @@ const updatePaddingInner = () => {
stylesheetStore.updateProperty(selector.value, 'padding', paddingInner.value.value, paddingInner.value.unit); stylesheetStore.updateProperty(selector.value, 'padding', paddingInner.value.value, paddingInner.value.unit);
}; };
// Apply all current field values to create/update the CSS block
const applyAllStyles = () => {
if (!selector.value) return;
updateFontFamily();
updateFontStyle();
updateFontWeight();
updateFontSize();
updateTextAlign();
updateColor();
updateBackground();
updateMarginOuter();
updatePaddingInner();
};
// Watch for changes // Watch for changes
watch(() => fontFamily.value.value, () => { watch(() => fontFamily.value.value, () => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
@ -605,6 +688,9 @@ const open = (element, event, count = null) => {
// Store instance count if provided, otherwise calculate it // Store instance count if provided, otherwise calculate it
elementInstanceCount.value = count !== null ? count : getInstanceCount(selector.value); elementInstanceCount.value = count !== null ? count : getInstanceCount(selector.value);
// Read inheritance state from element's data attribute
inheritanceLocked.value = element.dataset.inheritanceUnlocked !== 'true';
// Load values from stylesheet // Load values from stylesheet
loadValuesFromStylesheet(); loadValuesFromStylesheet();
@ -668,8 +754,25 @@ const handleIframeClick = (event, targetElement = null, elementCount = null) =>
}; };
const toggleInheritance = () => { const toggleInheritance = () => {
const wasLocked = inheritanceLocked.value;
inheritanceLocked.value = !inheritanceLocked.value; inheritanceLocked.value = !inheritanceLocked.value;
// TODO: Implement CSS priority logic when unlocked
// Store the inheritance state in the element's data attribute
if (selectedElement.value) {
if (inheritanceLocked.value) {
delete selectedElement.value.dataset.inheritanceUnlocked;
} else {
selectedElement.value.dataset.inheritanceUnlocked = 'true';
}
}
if (inheritanceLocked.value && !wasLocked) {
// Re-locking: remove the element-specific CSS block to restore inheritance
removeElementBlock();
} else if (!inheritanceLocked.value && wasLocked) {
// Unlocking: apply all current field values to create the CSS block
applyAllStyles();
}
}; };
defineExpose({ handleIframeClick, close, visible }); defineExpose({ handleIframeClick, close, visible });

View file

@ -285,6 +285,7 @@
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet'; import { useStylesheetStore } from '../stores/stylesheet';
import { usePopupPosition } from '../composables/usePopupPosition'; import { usePopupPosition } from '../composables/usePopupPosition';
import { useDebounce } from '../composables/useDebounce';
import NumberInput from './ui/NumberInput.vue'; import NumberInput from './ui/NumberInput.vue';
import Coloris from '@melloware/coloris'; import Coloris from '@melloware/coloris';
import '@melloware/coloris/dist/coloris.css'; import '@melloware/coloris/dist/coloris.css';
@ -312,7 +313,7 @@ const inheritanceLocked = ref(true);
const backgroundColorInput = ref(null); const backgroundColorInput = ref(null);
let isUpdatingFromStore = false; let isUpdatingFromStore = false;
let updateTimer = null; const { debouncedUpdate } = useDebounce(500);
const margins = ref({ const margins = ref({
top: { value: 0, unit: 'mm' }, top: { value: 0, unit: 'mm' },
@ -328,11 +329,6 @@ const background = ref({
const pattern = ref(''); const pattern = ref('');
const debouncedUpdate = (callback) => {
clearTimeout(updateTimer);
updateTimer = setTimeout(callback, 1000);
};
const immediateUpdate = (callback) => { const immediateUpdate = (callback) => {
callback(); callback();
}; };
@ -387,9 +383,9 @@ const removeTemplateBlock = () => {
} }
}; };
const updateMargins = () => { const updateMargins = (force = false) => {
// Only update if inheritance is unlocked // Only update if inheritance is unlocked (unless forced)
if (inheritanceLocked.value) return; if (!force && inheritanceLocked.value) 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}`; 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}`;
@ -419,9 +415,9 @@ const updateMargins = () => {
} }
}; };
const updateBackground = () => { const updateBackground = (force = false) => {
// Only update if inheritance is unlocked // Only update if inheritance is unlocked (unless forced)
if (inheritanceLocked.value) return; if (!force && inheritanceLocked.value) return;
if (!background.value.value) return; if (!background.value.value) return;
const currentBlock = getOrCreateTemplateBlock(); const currentBlock = getOrCreateTemplateBlock();
@ -448,6 +444,12 @@ const updateBackground = () => {
} }
}; };
// Apply all current field values to create/update the CSS block
const applyAllStyles = () => {
updateMargins(true);
updateBackground(true);
};
// Watch margin values (number inputs) with debounce // Watch margin values (number inputs) with debounce
watch( watch(
() => [ () => [
@ -536,8 +538,8 @@ const open = (pageElement, event, count = 1) => {
// Extract template name from data-page-type attribute // Extract template name from data-page-type attribute
templateName.value = pageElement.getAttribute('data-page-type') || ''; templateName.value = pageElement.getAttribute('data-page-type') || '';
// Reset inheritance state when opening // Read inheritance state from page element's data attribute
inheritanceLocked.value = true; inheritanceLocked.value = pageElement.dataset.inheritanceUnlocked !== 'true';
// Load values from stylesheet (@page block) // Load values from stylesheet (@page block)
loadValuesFromStylesheet(); loadValuesFromStylesheet();
@ -587,12 +589,22 @@ const toggleInheritance = () => {
const wasLocked = inheritanceLocked.value; const wasLocked = inheritanceLocked.value;
inheritanceLocked.value = !inheritanceLocked.value; inheritanceLocked.value = !inheritanceLocked.value;
// Store the inheritance state in the page element's data attribute
if (selectedPageElement.value) {
if (inheritanceLocked.value) {
delete selectedPageElement.value.dataset.inheritanceUnlocked;
} else {
selectedPageElement.value.dataset.inheritanceUnlocked = 'true';
}
}
if (inheritanceLocked.value && !wasLocked) { if (inheritanceLocked.value && !wasLocked) {
// Re-locking: remove the template-specific block // Re-locking: remove the template-specific block
// Fields keep their values, but preview returns to @page defaults
removeTemplateBlock(); removeTemplateBlock();
} else if (!inheritanceLocked.value && wasLocked) {
// Unlocking: apply all current field values to create the CSS block
applyAllStyles();
} }
// When unlocking: fields already have values, block will be created on first edit
}; };
const pageCss = computed(() => { const pageCss = computed(() => {
@ -604,9 +616,47 @@ const pageCss = computed(() => {
return stylesheetStore.extractBlock('@page') || ''; return stylesheetStore.extractBlock('@page') || '';
}); });
// Generate a preview CSS block from current field values
const generatePreviewCss = () => {
if (!templateName.value) return '';
const properties = [];
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}`;
properties.push(` margin: ${marginValue};`);
if (background.value.value) {
properties.push(` background: ${background.value.value};`);
}
if (properties.length === 0) return '';
return `@page ${templateName.value} {\n${properties.join('\n')}\n}`;
};
const displayedCss = computed(() => {
// If unlocked, show the actual CSS block from stylesheet
if (!inheritanceLocked.value) {
return pageCss.value;
}
// If locked, show commented preview of what would be applied
if (!templateName.value) {
// For base @page, just show it normally
return pageCss.value;
}
const preview = generatePreviewCss();
if (!preview) return pageCss.value;
return '/* Héritage verrouillé - déverrouiller pour appliquer */\n/* ' +
preview.split('\n').join('\n ') +
' */';
});
const highlightedCss = computed(() => { const highlightedCss = computed(() => {
if (!pageCss.value) return ''; if (!displayedCss.value) return '';
return hljs.highlight(pageCss.value, { language: 'css' }).value; return hljs.highlight(displayedCss.value, { language: 'css' }).value;
}); });
let cssDebounceTimer = null; let cssDebounceTimer = null;
@ -619,6 +669,7 @@ const handleCssInput = (event) => {
} }
cssDebounceTimer = setTimeout(() => { cssDebounceTimer = setTimeout(() => {
// Get the actual CSS block (not the commented preview)
const oldBlock = pageCss.value; const oldBlock = pageCss.value;
if (oldBlock) { if (oldBlock) {
stylesheetStore.content = stylesheetStore.content.replace( stylesheetStore.content = stylesheetStore.content.replace(

View file

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

View file

@ -288,16 +288,17 @@
<script setup> <script setup>
import { ref, computed, watch, onMounted, inject } from 'vue'; import { ref, computed, watch, onMounted, inject } from 'vue';
import { useStylesheetStore } from '../../stores/stylesheet'; import { useStylesheetStore } from '../../stores/stylesheet';
import { useDebounce } from '../../composables/useDebounce';
import Coloris from '@melloware/coloris'; import Coloris from '@melloware/coloris';
import NumberInput from '../ui/NumberInput.vue'; import NumberInput from '../ui/NumberInput.vue';
import '@melloware/coloris/dist/coloris.css'; import '@melloware/coloris/dist/coloris.css';
const stylesheetStore = useStylesheetStore(); const stylesheetStore = useStylesheetStore();
const { debouncedUpdate } = useDebounce(500);
const backgroundColorInput = ref(null); const backgroundColorInput = ref(null);
const activeTab = inject('activeTab', ref('document')); const activeTab = inject('activeTab', ref('document'));
let isUpdatingFromStore = false; let isUpdatingFromStore = false;
let updateTimer = null;
const pageFormat = ref('A4'); const pageFormat = ref('A4');
@ -328,11 +329,6 @@ const pattern = ref('');
const pageNumbers = ref(false); const pageNumbers = ref(false);
const runningTitle = ref(false); const runningTitle = ref(false);
const debouncedUpdate = (callback) => {
clearTimeout(updateTimer);
updateTimer = setTimeout(callback, 1000);
};
const immediateUpdate = (callback) => { const immediateUpdate = (callback) => {
callback(); callback();
}; };

View file

@ -34,7 +34,7 @@
<!-- Taille du texte --> <!-- Taille du texte -->
<div class="settings-subsection"> <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> <label for="text-size-range" class="label-with-tooltip" data-css="font-size">Taille du texte</label>
<InputWithUnit <InputWithUnit
v-model="fontSize" v-model="fontSize"
@ -130,9 +130,11 @@ import InputWithUnit from '../ui/InputWithUnit.vue';
import MarginEditor from '../ui/MarginEditor.vue'; import MarginEditor from '../ui/MarginEditor.vue';
import { useCssUpdater } from '../../composables/useCssUpdater'; import { useCssUpdater } from '../../composables/useCssUpdater';
import { useCssSync } from '../../composables/useCssSync'; import { useCssSync } from '../../composables/useCssSync';
import { useDebounce } from '../../composables/useDebounce';
const { updateStyle, setMargin, setDetailedMargins, setPadding, setDetailedPadding } = useCssUpdater(); const { updateStyle, setMargin, setDetailedMargins, setPadding, setDetailedPadding } = useCssUpdater();
const { extractValue, extractNumericValue, extractSpacing } = useCssSync(); const { extractValue, extractNumericValue, extractSpacing } = useCssSync();
const { debouncedUpdate } = useDebounce(500);
// Constants // Constants
const fonts = ['Alegreya Sans', 'Arial', 'Georgia', 'Helvetica', 'Times New Roman']; const fonts = ['Alegreya Sans', 'Arial', 'Georgia', 'Helvetica', 'Times New Roman'];
@ -203,26 +205,32 @@ watch(weight, (val) => {
watch(fontSize, (val) => { watch(fontSize, (val) => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
updateStyle('p', 'font-size', `${val.value}${val.unit}`); debouncedUpdate(() => {
updateStyle('p', 'font-size', `${val.value}${val.unit}`);
});
}, { deep: true }); }, { deep: true });
// Margin/Padding handlers // Margin/Padding handlers
const handleMarginOuterChange = ({ type, simple, detailed }) => { const handleMarginOuterChange = ({ type, simple, detailed }) => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
if (type === 'simple') { debouncedUpdate(() => {
setMargin('p', simple.value, simple.unit); if (type === 'simple') {
} else { setMargin('p', simple.value, simple.unit);
setDetailedMargins('p', detailed.top, detailed.right, detailed.bottom, detailed.left); } else {
} setDetailedMargins('p', detailed.top, detailed.right, detailed.bottom, detailed.left);
}
});
}; };
const handleMarginInnerChange = ({ type, simple, detailed }) => { const handleMarginInnerChange = ({ type, simple, detailed }) => {
if (isUpdatingFromStore) return; if (isUpdatingFromStore) return;
if (type === 'simple') { debouncedUpdate(() => {
setPadding('p', simple.value, simple.unit); if (type === 'simple') {
} else { setPadding('p', simple.value, simple.unit);
setDetailedPadding('p', detailed.top, detailed.right, detailed.bottom, detailed.left); } else {
} setDetailedPadding('p', detailed.top, detailed.right, detailed.bottom, detailed.left);
}
});
}; };
// Sync from store // Sync from store

View file

@ -0,0 +1,15 @@
/**
* Composable for debounced updates
* @param {number} delay - Debounce delay in milliseconds (default: 500ms)
* @returns {Function} debouncedUpdate function
*/
export function useDebounce(delay = 500) {
let timer = null;
const debouncedUpdate = (callback) => {
clearTimeout(timer);
timer = setTimeout(callback, delay);
};
return { debouncedUpdate };
}

View file

@ -16,20 +16,32 @@ const extractCssValue = (css, selector, property) => {
const updateCssValue = ({ css, selector, property, value, unit }) => { const updateCssValue = ({ css, selector, property, value, unit }) => {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const formattedValue = unit === '' || unit === undefined ? value : `${value}${unit}`;
if (unit === '') { // Check if selector exists
const regex = new RegExp( const selectorRegex = new RegExp(`${escaped}\\s*{([^}]*)}`, 'i');
`(${escaped}\\s*{[^}]*${property}:\\s*)[^;]+`, const selectorMatch = css.match(selectorRegex);
'gi'
); if (!selectorMatch) {
return css.replace(regex, `$1${value}`); // Selector doesn't exist, create it
return css + `\n${selector} {\n ${property}: ${formattedValue};\n}\n`;
} }
const regex = new RegExp( const blockContent = selectorMatch[1];
`(${escaped}\\s*{[^}]*${property}:\\s*)[\\d.]+(px|rem|em|mm|cm|in)`,
'gi' // Check if property exists in the block
); const propertyRegex = new RegExp(`(${property}\\s*:\\s*)([^;]+)(;?)`, 'i');
return css.replace(regex, `$1${value}${unit}`); const propertyMatch = blockContent.match(propertyRegex);
if (!propertyMatch) {
// Property doesn't exist, add it to the block
const newBlockContent = blockContent.trimEnd() + `\n ${property}: ${formattedValue};`;
return css.replace(selectorRegex, `${selector} {${newBlockContent}}`);
}
// Property exists, replace the entire value
const newBlockContent = blockContent.replace(propertyRegex, `$1${formattedValue}$3`);
return css.replace(selectorRegex, `${selector} {${newBlockContent}}`);
}; };
const cssParsingUtils = { extractCssBlock, extractCssValue, updateCssValue }; const cssParsingUtils = { extractCssBlock, extractCssValue, updateCssValue };