+
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateDetailedUnit(side.key, unit)"
+ />
+
+
+
+
+
+
+
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
+