From 94112ab1a896ff8b0fb30510f06f8b1423a59f4b Mon Sep 17 00:00:00 2001 From: isUnknown Date: Fri, 5 Dec 2025 16:23:42 +0100 Subject: [PATCH 1/4] feat: add automatic CSS formatting with Prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrates Prettier into the stylesheet store for automatic CSS formatting: - Installs prettier v3.7.4 with postcss plugin - Implements formatContent() function using Prettier API - Auto-formats CSS after 500ms of inactivity (debounced) - Prevents infinite loops with isFormatting flag - Ensures consistent indentation and line breaks - Cleans up extra blank lines and formatting issues This ensures the CSS in the Code tab stays clean and properly formatted after reactive edits from TextSettings and PageSettings panels. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package-lock.json | 16 ++++++++++++++++ package.json | 1 + src/stores/stylesheet.js | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b8b45d..7710fc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "highlight.js": "^11.11.1", "pagedjs": "^0.4.3", "pinia": "^3.0.4", + "prettier": "^3.7.4", "vue": "^3.5.24" }, "devDependencies": { @@ -1888,6 +1889,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", diff --git a/package.json b/package.json index 03f7c56..0efb5ff 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "highlight.js": "^11.11.1", "pagedjs": "^0.4.3", "pinia": "^3.0.4", + "prettier": "^3.7.4", "vue": "^3.5.24" }, "devDependencies": { diff --git a/src/stores/stylesheet.js b/src/stores/stylesheet.js index 2553818..d1b3408 100644 --- a/src/stores/stylesheet.js +++ b/src/stores/stylesheet.js @@ -1,9 +1,44 @@ import { defineStore } from 'pinia'; -import { ref } from 'vue'; +import { ref, watch } from 'vue'; import cssParsingUtils from '../utils/css-parsing'; +import prettier from 'prettier/standalone'; +import parserPostcss from 'prettier/plugins/postcss'; export const useStylesheetStore = defineStore('stylesheet', () => { const content = ref(''); + let formatTimer = null; + let isFormatting = false; + + // Format CSS with Prettier + const formatContent = async () => { + if (isFormatting || !content.value) return; + + try { + isFormatting = true; + const formatted = await prettier.format(content.value, { + parser: 'css', + plugins: [parserPostcss], + printWidth: 80, + tabWidth: 2, + useTabs: false, + }); + content.value = formatted; + } catch (error) { + console.error('CSS formatting error:', error); + } finally { + isFormatting = false; + } + }; + + // Watch content and format after 500ms of inactivity + watch(content, () => { + if (isFormatting) return; + + clearTimeout(formatTimer); + formatTimer = setTimeout(() => { + formatContent(); + }, 500); + }); const loadStylesheet = async () => { const response = await fetch('/assets/css/stylesheet.css'); @@ -33,6 +68,7 @@ export const useStylesheetStore = defineStore('stylesheet', () => { loadStylesheet, updateProperty, extractValue, - extractBlock + extractBlock, + formatContent }; }); From c4d2015a693f4b6d879d01de7ceeef683fbcbd4f Mon Sep 17 00:00:00 2001 From: isUnknown Date: Fri, 5 Dec 2025 16:30:44 +0100 Subject: [PATCH 2/4] refactor: extract reusable UI components and composables from TextSettings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major refactoring to improve code quality and reduce duplication: TextSettings.vue: 1127 → 269 lines (-76%) New composables: - useCssUpdater.js: generic CSS update/remove functions - useCssSync.js: CSS parsing to form fields New UI components: - UnitToggle.vue: reusable unit selector buttons - InputWithUnit.vue: number input with unit toggle - MarginEditor.vue: simple/detailed margin editor with sync Benefits: - Reusable components for other settings panels - Centralized CSS manipulation logic - Better separation of concerns - Easier to test and maintain 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/editor/TextSettings.vue | 1154 +++--------------------- src/components/ui/InputWithUnit.vue | 70 ++ src/components/ui/MarginEditor.vue | 162 ++++ src/components/ui/UnitToggle.vue | 28 + src/composables/useCssSync.js | 119 +++ src/composables/useCssUpdater.js | 120 +++ 6 files changed, 647 insertions(+), 1006 deletions(-) create mode 100644 src/components/ui/InputWithUnit.vue create mode 100644 src/components/ui/MarginEditor.vue create mode 100644 src/components/ui/UnitToggle.vue create mode 100644 src/composables/useCssSync.js create mode 100644 src/composables/useCssUpdater.js diff --git a/src/components/editor/TextSettings.vue b/src/components/editor/TextSettings.vue index 75538bf..92d77f5 100644 --- a/src/components/editor/TextSettings.vue +++ b/src/components/editor/TextSettings.vue @@ -6,16 +6,13 @@ pouvez modifier ensuite les éléments indépendamment.

+
@@ -25,122 +22,41 @@
+
-
- - - - - - - -
+
+
-
- - -
- - - -
-
+
+
+
@@ -148,20 +64,19 @@
-
@@ -169,782 +84,177 @@
+
-
- -
- -
- - -
- -
-
- -
-
- -
- -
- - -
-
-
- -
- -
- -
- - -
-
-
- -
- -
- -
- - -
-
-
- -
- -
- -
- - -
-
-
-
+
+
-
- -
- -
- - -
- -
-
- -
-
- -
- -
- - -
-
-
- -
- -
- -
- - -
-
-
- -
- -
- -
- - -
-
-
- -
- -
- -
- - -
-
-
-
+
diff --git a/src/components/ui/InputWithUnit.vue b/src/components/ui/InputWithUnit.vue new file mode 100644 index 0000000..c8605a3 --- /dev/null +++ b/src/components/ui/InputWithUnit.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/components/ui/MarginEditor.vue b/src/components/ui/MarginEditor.vue new file mode 100644 index 0000000..1802640 --- /dev/null +++ b/src/components/ui/MarginEditor.vue @@ -0,0 +1,162 @@ + + + diff --git a/src/components/ui/UnitToggle.vue b/src/components/ui/UnitToggle.vue new file mode 100644 index 0000000..17228cc --- /dev/null +++ b/src/components/ui/UnitToggle.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/composables/useCssSync.js b/src/composables/useCssSync.js new file mode 100644 index 0000000..96af721 --- /dev/null +++ b/src/composables/useCssSync.js @@ -0,0 +1,119 @@ +import { useStylesheetStore } from '../stores/stylesheet'; + +export function useCssSync() { + const store = useStylesheetStore(); + + /** + * Extract a simple CSS value (string) + */ + const extractValue = (selector, property) => { + const block = store.extractBlock(selector); + if (!block) return null; + + const match = block.match(new RegExp(`${property}:\\s*([^;]+)`, 'i')); + return match ? match[1].trim() : null; + }; + + /** + * Extract a numeric CSS value with unit + * Returns { value: number, unit: string } or null + */ + const extractNumericValue = (selector, property, allowedUnits = ['px', 'em', 'rem', 'mm']) => { + const block = store.extractBlock(selector); + if (!block) return null; + + const unitsPattern = allowedUnits.join('|'); + const match = block.match(new RegExp(`${property}:\\s*([0-9.]+)(${unitsPattern})`, 'i')); + + if (match) { + return { + value: parseFloat(match[1]), + unit: match[2].toLowerCase() + }; + } + return null; + }; + + /** + * Extract margin/padding shorthand (handles 1 or 4 values) + * Returns { simple: { value, unit } } or { detailed: { top, right, bottom, left } } + */ + const extractSpacing = (selector, property, allowedUnits = ['mm', 'px']) => { + const block = store.extractBlock(selector); + if (!block) return null; + + const unitsPattern = allowedUnits.join('|'); + + // Check for detailed properties first (property-top, property-right, etc.) + const topMatch = block.match(new RegExp(`${property}-top:\\s*([0-9.]+)(${unitsPattern})`, 'i')); + + if (topMatch) { + const rightMatch = block.match(new RegExp(`${property}-right:\\s*([0-9.]+)(${unitsPattern})`, 'i')); + const bottomMatch = block.match(new RegExp(`${property}-bottom:\\s*([0-9.]+)(${unitsPattern})`, 'i')); + const leftMatch = block.match(new RegExp(`${property}-left:\\s*([0-9.]+)(${unitsPattern})`, 'i')); + + return { + detailed: { + top: topMatch ? { value: parseFloat(topMatch[1]), unit: topMatch[2] } : { value: 0, unit: 'mm' }, + right: rightMatch ? { value: parseFloat(rightMatch[1]), unit: rightMatch[2] } : { value: 0, unit: 'mm' }, + bottom: bottomMatch ? { value: parseFloat(bottomMatch[1]), unit: bottomMatch[2] } : { value: 0, unit: 'mm' }, + left: leftMatch ? { value: parseFloat(leftMatch[1]), unit: leftMatch[2] } : { value: 0, unit: 'mm' }, + } + }; + } + + // Check for shorthand property + const shorthandMatch = block.match(new RegExp(`${property}:\\s*([^;]+)`, 'i')); + if (!shorthandMatch) return null; + + const shorthandValue = shorthandMatch[1].trim(); + + // Check for 4-value format: "0mm 0mm 24mm 0mm" (top right bottom left) + const fourValuePattern = new RegExp( + `^([0-9.]+)(${unitsPattern})\\s+([0-9.]+)(${unitsPattern})\\s+([0-9.]+)(${unitsPattern})\\s+([0-9.]+)(${unitsPattern})$`, + 'i' + ); + const fourValueMatch = shorthandValue.match(fourValuePattern); + + if (fourValueMatch) { + return { + detailed: { + top: { value: parseFloat(fourValueMatch[1]), unit: fourValueMatch[2] }, + right: { value: parseFloat(fourValueMatch[3]), unit: fourValueMatch[4] }, + bottom: { value: parseFloat(fourValueMatch[5]), unit: fourValueMatch[6] }, + left: { value: parseFloat(fourValueMatch[7]), unit: fourValueMatch[8] }, + } + }; + } + + // Single value format: "10mm" + const singleValuePattern = new RegExp(`^([0-9.]+)(${unitsPattern})$`, 'i'); + const singleValueMatch = shorthandValue.match(singleValuePattern); + + if (singleValueMatch) { + return { + simple: { + value: parseFloat(singleValueMatch[1]), + unit: singleValueMatch[2] + } + }; + } + + return null; + }; + + /** + * Check if a property value equals a specific string + */ + const hasValue = (selector, property, expectedValue) => { + const value = extractValue(selector, property); + return value === expectedValue; + }; + + return { + extractValue, + extractNumericValue, + extractSpacing, + hasValue, + }; +} diff --git a/src/composables/useCssUpdater.js b/src/composables/useCssUpdater.js new file mode 100644 index 0000000..2eb6018 --- /dev/null +++ b/src/composables/useCssUpdater.js @@ -0,0 +1,120 @@ +import { useStylesheetStore } from '../stores/stylesheet'; + +export function useCssUpdater() { + const store = useStylesheetStore(); + + /** + * Update or add a CSS property for a given selector + */ + const updateStyle = (selector, property, value) => { + const currentBlock = store.extractBlock(selector) || createRule(selector); + + if (currentBlock.includes(`${property}:`)) { + const updatedBlock = currentBlock.replace( + new RegExp(`(${property}:\\s*)[^;]+`, 'i'), + `$1${value}` + ); + store.content = store.content.replace(currentBlock, updatedBlock); + } else { + const updatedBlock = currentBlock.replace( + /(\s*})$/, + ` ${property}: ${value};\n$1` + ); + store.content = store.content.replace(currentBlock, updatedBlock); + } + }; + + /** + * Remove a CSS property from a selector + */ + const removeProperty = (selector, property) => { + const currentBlock = store.extractBlock(selector); + if (!currentBlock) return; + + const updatedBlock = currentBlock.replace( + new RegExp(`\\s*${property}:\\s*[^;]+;\\n?`, 'gi'), + '' + ); + + if (updatedBlock !== currentBlock) { + store.content = store.content.replace(currentBlock, updatedBlock); + } + }; + + /** + * Remove multiple CSS properties from a selector + */ + const removeProperties = (selector, properties) => { + let currentBlock = store.extractBlock(selector); + if (!currentBlock) return; + + let updatedBlock = currentBlock; + for (const property of properties) { + updatedBlock = updatedBlock.replace( + new RegExp(`\\s*${property}:\\s*[^;]+;\\n?`, 'gi'), + '' + ); + } + + if (updatedBlock !== currentBlock) { + store.content = store.content.replace(currentBlock, updatedBlock); + } + }; + + /** + * Create a new CSS rule for a selector + */ + const createRule = (selector) => { + store.content += `\n\n${selector} {\n}\n`; + return `${selector} {\n}`; + }; + + /** + * Remove detailed margin properties and set shorthand + */ + const setMargin = (selector, value, unit) => { + removeProperties(selector, ['margin-top', 'margin-right', 'margin-bottom', 'margin-left']); + updateStyle(selector, 'margin', `${value}${unit}`); + }; + + /** + * Remove shorthand margin and set detailed margins + */ + const setDetailedMargins = (selector, top, right, bottom, left) => { + removeProperty(selector, 'margin'); + updateStyle(selector, 'margin-top', `${top.value}${top.unit}`); + updateStyle(selector, 'margin-right', `${right.value}${right.unit}`); + updateStyle(selector, 'margin-bottom', `${bottom.value}${bottom.unit}`); + updateStyle(selector, 'margin-left', `${left.value}${left.unit}`); + }; + + /** + * Remove detailed padding properties and set shorthand + */ + const setPadding = (selector, value, unit) => { + removeProperties(selector, ['padding-top', 'padding-right', 'padding-bottom', 'padding-left']); + updateStyle(selector, 'padding', `${value}${unit}`); + }; + + /** + * Remove shorthand padding and set detailed padding + */ + const setDetailedPadding = (selector, top, right, bottom, left) => { + removeProperty(selector, 'padding'); + updateStyle(selector, 'padding-top', `${top.value}${top.unit}`); + updateStyle(selector, 'padding-right', `${right.value}${right.unit}`); + updateStyle(selector, 'padding-bottom', `${bottom.value}${bottom.unit}`); + updateStyle(selector, 'padding-left', `${left.value}${left.unit}`); + }; + + return { + updateStyle, + removeProperty, + removeProperties, + createRule, + setMargin, + setDetailedMargins, + setPadding, + setDetailedPadding, + }; +} From d3cd296fd7cc66bb91edc059e5bc451c6b08b69d Mon Sep 17 00:00:00 2001 From: isUnknown Date: Fri, 5 Dec 2025 16:35:53 +0100 Subject: [PATCH 3/4] fix: initialize margin fields from stylesheet detailed values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes margin/padding field initialization when CSS contains 4-value shorthand (e.g., margin: 0mm 0mm 24mm 0mm). Now properly populates both simple and detailed fields, and auto-opens detailed editor when values differ. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/editor/TextSettings.vue | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/components/editor/TextSettings.vue b/src/components/editor/TextSettings.vue index 92d77f5..d8768ba 100644 --- a/src/components/editor/TextSettings.vue +++ b/src/components/editor/TextSettings.vue @@ -87,6 +87,7 @@
{ if (margins) { if (margins.simple) { marginOuter.value = margins.simple; + // Sync detailed from simple + marginOuterDetailed.value = { + top: { ...margins.simple }, + right: { ...margins.simple }, + bottom: { ...margins.simple }, + left: { ...margins.simple } + }; } else if (margins.detailed) { marginOuterDetailed.value = margins.detailed; + // Check if all values are the same to set simple value + const allSame = + margins.detailed.top.value === margins.detailed.right.value && + margins.detailed.top.value === margins.detailed.bottom.value && + margins.detailed.top.value === margins.detailed.left.value && + 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); + } } } @@ -247,8 +280,36 @@ const syncFromStore = () => { if (padding) { if (padding.simple) { marginInner.value = padding.simple; + // Sync detailed from simple + marginInnerDetailed.value = { + top: { ...padding.simple }, + right: { ...padding.simple }, + bottom: { ...padding.simple }, + left: { ...padding.simple } + }; } else if (padding.detailed) { marginInnerDetailed.value = padding.detailed; + // Check if all values are the same to set simple value + const allSame = + padding.detailed.top.value === padding.detailed.right.value && + padding.detailed.top.value === padding.detailed.bottom.value && + padding.detailed.top.value === padding.detailed.left.value && + 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); + } } } From b584a539fe7c4c8eab71a7693ea561b1947e3c69 Mon Sep 17 00:00:00 2001 From: isUnknown Date: Fri, 5 Dec 2025 16:38:29 +0100 Subject: [PATCH 4/4] feat: add rem unit option to margins in PageSettings and TextSettings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 'rem' as an available unit for margin inputs in both components. Updates useCssSync composable to parse rem values from CSS. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/editor/PageSettings.vue | 28 ++++++++++++++++++++++++++ src/components/editor/TextSettings.vue | 2 ++ src/composables/useCssSync.js | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/components/editor/PageSettings.vue b/src/components/editor/PageSettings.vue index 17cab6e..616775b 100644 --- a/src/components/editor/PageSettings.vue +++ b/src/components/editor/PageSettings.vue @@ -66,6 +66,13 @@ > px +
@@ -94,6 +101,13 @@ > px +
@@ -122,6 +136,13 @@ > px + @@ -150,6 +171,13 @@ > px + diff --git a/src/components/editor/TextSettings.vue b/src/components/editor/TextSettings.vue index d8768ba..614b79d 100644 --- a/src/components/editor/TextSettings.vue +++ b/src/components/editor/TextSettings.vue @@ -92,6 +92,7 @@ label="Marges extérieures" v-model:simple="marginOuter" v-model:detailed="marginOuterDetailed" + :units="['mm', 'px', 'rem']" @change="handleMarginOuterChange" /> @@ -104,6 +105,7 @@ label="Marges intérieures" v-model:simple="marginInner" v-model:detailed="marginInnerDetailed" + :units="['mm', 'px', 'rem']" @change="handleMarginInnerChange" /> diff --git a/src/composables/useCssSync.js b/src/composables/useCssSync.js index 96af721..3588635 100644 --- a/src/composables/useCssSync.js +++ b/src/composables/useCssSync.js @@ -38,7 +38,7 @@ export function useCssSync() { * Extract margin/padding shorthand (handles 1 or 4 values) * Returns { simple: { value, unit } } or { detailed: { top, right, bottom, left } } */ - const extractSpacing = (selector, property, allowedUnits = ['mm', 'px']) => { + const extractSpacing = (selector, property, allowedUnits = ['mm', 'px', 'rem']) => { const block = store.extractBlock(selector); if (!block) return null;