3 values systeme for heritage

This commit is contained in:
Julie Blanc 2026-03-05 19:00:29 +01:00
parent 449f0eda31
commit bc2317ab69
4 changed files with 280 additions and 215 deletions

View file

@ -481,6 +481,7 @@ input[type=number] {
.settings-subsection {
padding: var(--space-xs) 0;
padding-left: 30px;
}
.settings-subsection h3 {
margin-top: calc(var(--space-xs) * 1.5);

View file

@ -346,6 +346,31 @@ const settingCache = reactive({
color: null, // string
});
// Persistent state per element selector (survives open/close)
const elementStates = new Map();
const saveElementState = () => {
if (!selector.value) return;
elementStates.set(selector.value, {
toggles: { ...settingEnabled },
cache: {
font: settingCache.font ? { ...settingCache.font } : null,
fontSize: settingCache.fontSize ? { ...settingCache.fontSize } : null,
lineHeight: settingCache.lineHeight ? { ...settingCache.lineHeight } : null,
color: settingCache.color ?? null,
},
// Store current values for enabled groups (survives external CSS changes on shared selectors)
values: {
fontFamily: fontFamily.value,
italic: italic.value,
bold: bold.value,
fontSize: { value: fontSize.value, unit: fontSize.unit },
lineHeight: { value: lineHeight.value, unit: lineHeight.unit },
color: color.value,
},
});
};
// Per-subsection toggle state all unchecked by default
// Special groups (font, fontSize, lineHeight, color) appear in popup-css with "valeur par défaut" comment
const settingEnabled = reactive({
@ -362,7 +387,7 @@ const settingEnabled = reactive({
// Style property descriptors (with group field)
const styleProps = [
{ css: 'font-family', group: 'font', get: () => fontFamily.value === 'sans-serif' ? 'sans-serif' : `"${fontFamily.value}"`, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false },
{ css: 'font-family', group: 'font', get: () => fontFamily.value === 'sans-serif' ? 'sans-serif' : `"${fontFamily.value}"`, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false, skipWatch: true },
{ css: 'font-style', group: 'font', get: () => italic.value ? 'italic' : 'normal', set: v => italic.value = v === 'italic', debounce: false, skipWhenDefault: v => v !== 'italic' },
{ css: 'font-weight', group: 'font', get: () => bold.value ? 'bold' : 'normal', set: v => bold.value = v === 'bold' || parseInt(v) >= 700, debounce: false, skipWhenDefault: v => v === 'normal' },
{ css: 'text-align', group: 'textAlign', get: () => textAlign.value, set: v => textAlign.value = v, debounce: false },
@ -501,90 +526,74 @@ const elementCss = computed(() => {
return stylesheetStore.extractBlock(selector.value) || '';
});
const generatePreviewCss = () => {
if (!selector.value) return '';
const properties = [];
for (const prop of styleProps) {
if (!settingEnabled[prop.group]) continue;
const val = prop.get();
if (!val) continue;
if (prop.css === 'font-style' && val !== 'italic') continue;
if (prop.css === 'font-weight' && val === 'normal') continue;
if ((prop.css === 'border-style' || prop.css === 'border-color') && borderWidth.value === 0) continue;
properties.push(` ${prop.css}: ${val};`);
}
for (const prop of unitProps) {
if (!settingEnabled[prop.group]) continue;
if (prop.ref.value !== undefined && prop.ref.value !== null) {
properties.push(` ${prop.css}: ${prop.ref.value}${prop.ref.unit};`);
}
}
if (settingEnabled.margin) {
for (const side of ['top', 'right', 'bottom', 'left']) {
if (margin[side].value !== undefined && margin[side].value !== null) {
properties.push(` margin-${side}: ${margin[side].value}${margin[side].unit};`);
}
}
}
if (settingEnabled.padding) {
for (const side of ['top', 'right', 'bottom', 'left']) {
if (padding[side].value !== undefined && padding[side].value !== null) {
properties.push(` padding-${side}: ${padding[side].value}${padding[side].unit};`);
}
}
}
if (properties.length === 0) return '';
return `${selector.value} {\n${properties.join('\n')}\n}`;
};
// Canonical property order matching the template settings-subsections
const displayedCssOrder = [
// font group
{ css: 'font-family', group: 'font', special: true,
getValue: () => { const val = settingEnabled.font ? fontFamily.value : textDefaults.fontFamily; return val === 'sans-serif' ? 'sans-serif' : `"${val}"`; } },
{ css: 'font-style', group: 'font',
getValue: () => italic.value ? 'italic' : null,
skip: () => !settingEnabled.font || !italic.value },
{ css: 'font-weight', group: 'font',
getValue: () => bold.value ? 'bold' : null,
skip: () => !settingEnabled.font || !bold.value },
// fontSize group
{ css: 'font-size', group: 'fontSize', special: true,
getValue: () => `${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).value}${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).unit}` },
// lineHeight group
{ css: 'line-height', group: 'lineHeight', special: true,
getValue: () => `${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).value}${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).unit}` },
// textAlign group
{ css: 'text-align', group: 'textAlign',
getValue: () => textAlign.value,
skip: () => !settingEnabled.textAlign },
// color group
{ css: 'color', group: 'color', special: true,
getValue: () => settingEnabled.color ? color.value : textDefaults.color },
// background group
{ css: 'background', group: 'background',
getValue: () => background.value,
skip: () => !settingEnabled.background },
// border group
{ css: 'border-width', group: 'border',
getValue: () => `${borderWidth.value}${borderWidth.unit}`,
skip: () => !settingEnabled.border },
{ css: 'border-style', group: 'border',
getValue: () => borderStyle.value,
skip: () => !settingEnabled.border || borderWidth.value === 0 },
{ css: 'border-color', group: 'border',
getValue: () => borderColor.value,
skip: () => !settingEnabled.border || borderWidth.value === 0 },
];
const displayedCss = computed(() => {
if (!selector.value) return '';
let base = elementCss.value || generatePreviewCss();
if (!base) base = `${selector.value} {\n}`;
const lines = [];
// Special groups config for displayedCss annotation
const specialDisplayProps = [
{ group: 'fontSize', css: 'font-size', getValue: () => `${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).value}${(settingEnabled.fontSize ? fontSize : textDefaults.fontSize).unit}` },
{ group: 'lineHeight', css: 'line-height', getValue: () => `${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).value}${(settingEnabled.lineHeight ? lineHeight : textDefaults.lineHeight).unit}` },
{ group: 'color', css: 'color', getValue: () => settingEnabled.color ? color.value : textDefaults.color },
{ group: 'font', css: 'font-family', getValue: () => { const val = settingEnabled.font ? fontFamily.value : textDefaults.fontFamily; return val === 'sans-serif' ? 'sans-serif' : `"${val}"`; } },
];
// For disabled special groups: annotate existing lines with comment
// For enabled special groups: remove stale comments
for (const sp of specialDisplayProps) {
if (!settingEnabled[sp.group] && base.includes(sp.css + ':')) {
const escaped = sp.css.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
base = base.replace(new RegExp(`(${escaped}:\\s*[^;]+;)(?!\\s*\\/\\*)`), '$1 /* valeur par défaut */');
for (const entry of displayedCssOrder) {
if (entry.skip && entry.skip()) continue;
const val = entry.getValue();
if (val === null || val === undefined) continue;
const comment = (entry.special && !settingEnabled[entry.group]) ? ' /* valeur par défaut */' : '';
lines.push(` ${entry.css}: ${val};${comment}`);
}
if (settingEnabled[sp.group]) {
const escaped = sp.css.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
base = base.replace(new RegExp(`(${escaped}:\\s*[^;]+;)\\s*\\/\\* valeur par défaut \\*\\/`), '$1');
// margin
if (settingEnabled.margin) {
for (const side of ['top', 'right', 'bottom', 'left']) {
lines.push(` margin-${side}: ${margin[side].value}${margin[side].unit};`);
}
}
// Inject missing special properties (if not yet in block at all)
const toInject = [];
for (const sp of specialDisplayProps) {
if (!base.includes(sp.css + ':')) {
const comment = !settingEnabled[sp.group] ? ' /* valeur par défaut */' : '';
toInject.push(` ${sp.css}: ${sp.getValue()};${comment}`);
// padding
if (settingEnabled.padding) {
for (const side of ['top', 'right', 'bottom', 'left']) {
lines.push(` padding-${side}: ${padding[side].value}${padding[side].unit};`);
}
}
if (toInject.length > 0) {
base = base.replace(/\}\s*$/, toInject.join('\n') + '\n}');
}
return base;
return `${selector.value} {\n${lines.join('\n')}\n}`;
});
// Apply all properties for a given group to the stylesheet
@ -652,19 +661,24 @@ const restoreFromCache = (group) => {
}
};
// Replace a special group's CSS values with TextSettings defaults
const applyDefaultsForGroup = (group) => {
// Remove a special group's CSS properties so the element inherits from TextSettings defaults
// Then re-write TextSettings values on their target selectors (p/body) to prevent
// shared selector conflicts (e.g. popup on `p` removing TextSettings' font-size)
const removeSpecialGroupProps = (group) => {
if (!selector.value) return;
if (group === 'fontSize') {
updateProp('font-size', textDefaults.fontSize.value, textDefaults.fontSize.unit);
removeProps(['font-size']);
stylesheetStore.updateProperty('p', 'font-size', textDefaults.fontSize.value, textDefaults.fontSize.unit);
} else if (group === 'lineHeight') {
updateProp('line-height', textDefaults.lineHeight.value, textDefaults.lineHeight.unit);
removeProps(['line-height']);
stylesheetStore.updateProperty('p', 'line-height', textDefaults.lineHeight.value, textDefaults.lineHeight.unit);
} else if (group === 'color') {
updateProp('color', textDefaults.color);
removeProps(['color']);
stylesheetStore.updateProperty('body', 'color', textDefaults.color);
} else if (group === 'font') {
removeProps(['font-family', 'font-style', 'font-weight']);
const fontVal = textDefaults.fontFamily === 'sans-serif' ? 'sans-serif' : `"${textDefaults.fontFamily}"`;
updateProp('font-family', fontVal);
removeProps(['font-style', 'font-weight']);
stylesheetStore.updateProperty('body', 'font-family', fontVal);
}
};
@ -679,41 +693,47 @@ const onToggleSetting = (group, enabled) => {
saveToCache(group);
const specialGroups = ['font', 'fontSize', 'lineHeight', 'color'];
if (specialGroups.includes(group)) {
applyDefaultsForGroup(group);
removeSpecialGroupProps(group);
} else {
removeProps(settingGroups[group]);
}
}
saveElementState();
nextTick(() => { isUpdatingFromStore = false; });
};
// Load font when fontFamily changes
// Load font when fontFamily changes, then update CSS (after font-face is ready)
watch(fontFamily, async (val) => {
if (val && val !== 'sans-serif') await loadFont(val);
if (isUpdatingFromStore) return;
if (!settingEnabled.font) return;
const cssValue = val === 'sans-serif' ? 'sans-serif' : `"${val}"`;
updateProp('font-family', cssValue);
});
// Sync disabled special groups with TextSettings defaults
// Sync special groups fields with TextSettings only when toggle is OFF and no cached element value
watch(() => textDefaults.fontFamily, (val) => {
if (!settingEnabled.font) fontFamily.value = val;
if (!settingEnabled.font && settingCache.font === null) fontFamily.value = val;
});
watch(() => textDefaults.fontSize, (val) => {
if (!settingEnabled.fontSize) {
if (!settingEnabled.fontSize && settingCache.fontSize === null) {
fontSize.value = val.value;
fontSize.unit = val.unit;
}
}, { deep: true });
watch(() => textDefaults.lineHeight, (val) => {
if (!settingEnabled.lineHeight) {
if (!settingEnabled.lineHeight && settingCache.lineHeight === null) {
lineHeight.value = val.value;
lineHeight.unit = val.unit;
}
}, { deep: true });
watch(() => textDefaults.color, (val) => {
if (!settingEnabled.color) color.value = val;
if (!settingEnabled.color && settingCache.color === null) color.value = val;
});
// Watchers simple props (with group guard)
for (const prop of styleProps) {
if (prop.skipWatch) continue;
watch(prop.get, () => {
if (isUpdatingFromStore) return;
if (!settingEnabled[prop.group]) return;
@ -792,16 +812,48 @@ const handleCssInput = (newCss) => {
}
};
// Watch stylesheet changes to sync values
// Re-apply stored enabled properties for all elements with active toggles
// Protects popup values when TextSettings writes to shared selectors (e.g. `p`)
const reapplyStoredEnabledGroups = () => {
const currentSel = basePopup.value?.visible ? selector.value : null;
for (const [sel, state] of elementStates) {
if (!state.toggles || !state.values) continue;
if (sel === currentSel) continue; // handled by applyAllEnabledGroups below
if (state.toggles.font) {
const val = state.values.fontFamily === 'sans-serif' ? 'sans-serif' : `"${state.values.fontFamily}"`;
stylesheetStore.updateProperty(sel, 'font-family', val);
}
if (state.toggles.fontSize) {
stylesheetStore.updateProperty(sel, 'font-size', state.values.fontSize.value, state.values.fontSize.unit);
}
if (state.toggles.lineHeight) {
stylesheetStore.updateProperty(sel, 'line-height', state.values.lineHeight.value, state.values.lineHeight.unit);
}
if (state.toggles.color) {
stylesheetStore.updateProperty(sel, 'color', state.values.color);
}
}
// For visible popup, use live refs
if (currentSel) {
applyAllEnabledGroups();
}
};
// Watch stylesheet changes: re-apply all stored enabled properties
// flush: 'sync' ensures this runs BEFORE the preview renderer watcher,
// so the corrected CSS is what gets rendered (avoids race condition)
watch(
() => stylesheetStore.customCss,
() => {
if (basePopup.value?.visible && !isUpdatingFromStore) {
if (isUpdatingFromStore) return;
isUpdatingFromStore = true;
loadValuesFromStylesheet();
reapplyStoredEnabledGroups();
nextTick(() => { isUpdatingFromStore = false; });
}
}
},
{ flush: 'sync' }
);
// Also watch when exiting edit mode
@ -819,97 +871,49 @@ watch(
const loadValuesFromStylesheet = (isInitialLoad = false) => {
if (!selector.value) return;
const groupsFound = new Set();
// Only detect settingEnabled from the custom CSS block (not baseCss fallback)
const customCssBlock = (() => {
const css = stylesheetStore.customCss;
if (!css) return '';
const escaped = selector.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = new RegExp(escaped + '\\s*\\{([^}]*)\\}').exec(css);
return match ? match[1] : '';
})();
const isInCustomCss = (cssProp) =>
customCssBlock.includes(cssProp + ':') || customCssBlock.includes(cssProp + ' :');
// During live sync, don't update any refs the popup's state is the source of truth
if (!isInitialLoad) return;
try {
// Simple props
// Simple props read from element's CSS block
for (const prop of styleProps) {
const data = stylesheetStore.extractValue(selector.value, prop.css);
if (data) {
if (isInCustomCss(prop.css)) groupsFound.add(prop.group);
// During live sync, don't overwrite refs for disabled groups (they show cached/fallback values)
if (isInitialLoad || settingEnabled[prop.group]) {
const value = typeof data === 'string' ? data : data.value;
prop.set(value);
}
}
}
// Unit props
for (const prop of unitProps) {
const data = stylesheetStore.extractValue(selector.value, prop.css);
if (data && data.value !== undefined) {
if (isInCustomCss(prop.css)) groupsFound.add(prop.group);
if (isInitialLoad || settingEnabled[prop.group]) {
prop.ref.value = data.value;
prop.ref.unit = data.unit;
}
}
}
// Margin sides try individual first, fallback to shorthand
const spacingSides = ['top', 'right', 'bottom', 'left'];
let anyMarginFound = false;
for (const side of spacingSides) {
const data = stylesheetStore.extractValue(selector.value, `margin-${side}`);
if (data && data.value !== undefined) {
margin[side].value = data.value;
margin[side].unit = data.unit;
if (isInCustomCss(`margin-${side}`)) anyMarginFound = true;
}
}
if (!anyMarginFound && isInCustomCss('margin')) {
const data = stylesheetStore.extractValue(selector.value, 'margin');
if (data && data.value !== undefined) {
for (const side of spacingSides) {
margin[side].value = data.value;
margin[side].unit = data.unit;
}
anyMarginFound = true;
}
}
if (anyMarginFound) groupsFound.add('margin');
// Padding sides try individual first, fallback to shorthand
let anyPaddingFound = false;
for (const side of spacingSides) {
const data = stylesheetStore.extractValue(selector.value, `padding-${side}`);
if (data && data.value !== undefined) {
padding[side].value = data.value;
padding[side].unit = data.unit;
if (isInCustomCss(`padding-${side}`)) anyPaddingFound = true;
}
}
if (!anyPaddingFound && isInCustomCss('padding')) {
const data = stylesheetStore.extractValue(selector.value, 'padding');
if (data && data.value !== undefined) {
for (const side of spacingSides) {
padding[side].value = data.value;
padding[side].unit = data.unit;
}
anyPaddingFound = true;
}
}
if (anyPaddingFound) groupsFound.add('padding');
// settingEnabled is NEVER modified automatically only by user toggle clicks
// Special groups: font, fontSize, color
// If not found in element CSS, load fallback values from TextSettings selectors
// Only on initial open during live sync, ElementPopup is independent from TextSettings
if (isInitialLoad) {
if (!groupsFound.has('font')) {
// Special groups: for disabled groups without cache, load fallback from TextSettings selectors
if (!settingEnabled.font && !settingCache.font) {
const ff = stylesheetStore.extractValue('body', 'font-family');
if (ff) fontFamily.value = (typeof ff === 'string' ? ff : ff.value).replace(/['"]/g, '');
const fs = stylesheetStore.extractValue('p', 'font-style');
@ -918,7 +922,7 @@ const loadValuesFromStylesheet = (isInitialLoad = false) => {
if (fw) { const v = typeof fw === 'string' ? fw : fw.value; bold.value = v === 'bold' || parseInt(v) >= 700; }
}
if (!groupsFound.has('fontSize')) {
if (!settingEnabled.fontSize && !settingCache.fontSize) {
const data = stylesheetStore.extractValue('p', 'font-size');
if (data && data.value !== undefined) {
fontSize.value = data.value;
@ -926,7 +930,7 @@ const loadValuesFromStylesheet = (isInitialLoad = false) => {
}
}
if (!groupsFound.has('lineHeight')) {
if (!settingEnabled.lineHeight && !settingCache.lineHeight) {
const data = stylesheetStore.extractValue('p', 'line-height');
if (data && data.value !== undefined) {
lineHeight.value = data.value;
@ -934,11 +938,10 @@ const loadValuesFromStylesheet = (isInitialLoad = false) => {
}
}
if (!groupsFound.has('color')) {
if (!settingEnabled.color && settingCache.color === null) {
const c = stylesheetStore.extractValue('body', 'color');
if (c) color.value = typeof c === 'string' ? c : c.value;
}
}
} catch (error) {
console.error('Error loading values from stylesheet:', error);
@ -946,31 +949,72 @@ const loadValuesFromStylesheet = (isInitialLoad = false) => {
};
const open = (element, event, count = null) => {
// Reset all toggles to unchecked for each new element
for (const group of Object.keys(settingEnabled)) {
settingEnabled[group] = false;
}
// Clear cache from any previous element
settingCache.font = null;
settingCache.fontSize = null;
settingCache.lineHeight = null;
settingCache.color = null;
isUpdatingFromStore = true;
selectedElement.value = element;
selector.value = getSelectorFromElement(element);
elementInstanceCount.value = count !== null ? count : getInstanceCount(selector.value);
loadValuesFromStylesheet(true);
basePopup.value.open(event);
// Restore persistent state for this element, or initialize all OFF
const stored = elementStates.get(selector.value);
if (stored) {
Object.assign(settingEnabled, stored.toggles);
settingCache.font = stored.cache.font ? { ...stored.cache.font } : null;
settingCache.fontSize = stored.cache.fontSize ? { ...stored.cache.fontSize } : null;
settingCache.lineHeight = stored.cache.lineHeight ? { ...stored.cache.lineHeight } : null;
settingCache.color = stored.cache.color ?? null;
} else {
for (const group of Object.keys(settingEnabled)) {
settingEnabled[group] = false;
}
settingCache.font = null;
settingCache.fontSize = null;
settingCache.lineHeight = null;
settingCache.color = null;
}
// Load CSS values for initial population
loadValuesFromStylesheet(true);
// Restore stored values: for enabled groups (CSS may have been overwritten by TextSettings)
// and for disabled groups with cache (field shows cached user value, grayed out)
if (stored && stored.values) {
if (settingEnabled.font) {
fontFamily.value = stored.values.fontFamily;
italic.value = stored.values.italic;
bold.value = stored.values.bold;
} else if (settingCache.font) {
fontFamily.value = settingCache.font.fontFamily;
italic.value = settingCache.font.italic;
bold.value = settingCache.font.bold;
}
if (settingEnabled.fontSize) {
fontSize.value = stored.values.fontSize.value;
fontSize.unit = stored.values.fontSize.unit;
} else if (settingCache.fontSize) {
fontSize.value = settingCache.fontSize.value;
fontSize.unit = settingCache.fontSize.unit;
}
if (settingEnabled.lineHeight) {
lineHeight.value = stored.values.lineHeight.value;
lineHeight.unit = stored.values.lineHeight.unit;
} else if (settingCache.lineHeight) {
lineHeight.value = settingCache.lineHeight.value;
lineHeight.unit = settingCache.lineHeight.unit;
}
if (settingEnabled.color) {
color.value = stored.values.color;
} else if (settingCache.color !== null) {
color.value = settingCache.color;
}
}
basePopup.value.open(event);
nextTick(() => { isUpdatingFromStore = false; });
};
const close = () => {
saveElementState();
basePopup.value?.close();
selector.value = '';
selectedElement.value = null;

View file

@ -72,7 +72,7 @@
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { ref, watch, onMounted, nextTick } from 'vue';
import { initColoris } from '../../composables/useColoris';
import InputWithUnit from '../ui/InputWithUnit.vue';
import { useCssUpdater } from '../../composables/useCssUpdater';
@ -87,51 +87,60 @@ const { debouncedUpdate } = useDebounce(500);
const textDefaults = useTextDefaults();
const { fonts: projectFonts, loadFont, loadAllFontPreviews } = useProjectFonts();
// State
const font = ref('sans-serif');
const fontSize = ref({ value: 16, unit: 'px' });
const lineHeight = ref({ value: 20, unit: 'px' });
// State initial values match stylesheet.print.css (overwritten by syncFromStore)
const font = ref('DM Sans');
const fontSize = ref({ value: 14, unit: 'px' });
const lineHeight = ref({ value: 18, unit: 'px' });
const color = ref('rgb(0, 0, 0)');
const colorInput = ref(null);
let isUpdatingFromStore = false;
// Start true to block immediate watchers from overwriting textDefaults during setup
let isUpdatingFromStore = true;
// Watchers for body styles
watch(font, async (val) => {
if (isUpdatingFromStore) return;
textDefaults.fontFamily = val;
if (val !== 'sans-serif') await loadFont(val);
if (isUpdatingFromStore) return;
const cssValue = val === 'sans-serif' ? 'sans-serif' : `"${val}"`;
updateStyle('body', 'font-family', cssValue);
}, { immediate: true });
watch(color, (val) => {
textDefaults.color = val;
if (isUpdatingFromStore) return;
textDefaults.color = val;
updateStyle('body', 'color', val);
}, { immediate: true });
watch(fontSize, (val) => {
textDefaults.fontSize = { value: val.value, unit: val.unit };
if (isUpdatingFromStore) return;
textDefaults.fontSize = { value: val.value, unit: val.unit };
debouncedUpdate(() => {
updateStyle('p', 'font-size', `${val.value}${val.unit}`);
});
}, { deep: true, immediate: true });
watch(lineHeight, (val) => {
textDefaults.lineHeight = { value: val.value, unit: val.unit };
if (isUpdatingFromStore) return;
textDefaults.lineHeight = { value: val.value, unit: val.unit };
debouncedUpdate(() => {
updateStyle('p', 'line-height', `${val.value}${val.unit}`);
});
}, { deep: true, immediate: true });
// Sync from store
// Sync from store (first mount) or from textDefaults (subsequent mounts)
const syncFromStore = () => {
isUpdatingFromStore = true;
if (textDefaults._initialized) {
// Already initialized restore from textDefaults (not from CSS which may contain popup values)
font.value = textDefaults.fontFamily;
fontSize.value = { value: textDefaults.fontSize.value, unit: textDefaults.fontSize.unit };
lineHeight.value = { value: textDefaults.lineHeight.value, unit: textDefaults.lineHeight.unit };
color.value = textDefaults.color;
} else {
// First mount read from CSS store
const fontVal = extractValue('body', 'font-family');
if (fontVal) {
const cleaned = fontVal.replace(/['"]/g, '').trim();
@ -141,13 +150,22 @@ const syncFromStore = () => {
const colorVal = extractValue('body', 'color');
if (colorVal) color.value = colorVal;
const fontSizeVal = extractNumericValue('p', 'font-size', ['px']); // ['px', 'em', 'rem']
const fontSizeVal = extractNumericValue('p', 'font-size', ['px']);
if (fontSizeVal) fontSize.value = fontSizeVal;
const lineHeightVal = extractNumericValue('p', 'line-height', ['px']);
if (lineHeightVal) lineHeight.value = lineHeightVal;
isUpdatingFromStore = false;
// Persist to textDefaults from CSS-synced values
textDefaults.fontFamily = font.value;
textDefaults.fontSize = { value: fontSize.value.value, unit: fontSize.value.unit };
textDefaults.lineHeight = { value: lineHeight.value.value, unit: lineHeight.value.unit };
textDefaults.color = color.value;
textDefaults._initialized = true;
}
// Release flag after watchers triggered by ref changes have been skipped
nextTick(() => { isUpdatingFromStore = false; });
};
const updateColorisButtons = () => {

View file

@ -1,11 +1,13 @@
import { reactive } from 'vue';
// Singleton reactive — TextSettings writes here, ElementPopup reads when disabled
// Initial values match stylesheet.print.css (overwritten by syncFromStore on first mount)
const defaults = reactive({
fontSize: { value: 16, unit: 'px' },
lineHeight: { value: 20, unit: 'px' },
fontSize: { value: 14, unit: 'px' },
lineHeight: { value: 18, unit: 'px' },
fontFamily: 'sans-serif',
color: 'rgb(0, 0, 0)',
_initialized: false,
});
export function useTextDefaults() {