+
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
+
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..3588635
--- /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', 'rem']) => {
+ 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,
+ };
+}
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
};
});