# ivyforms/1.0/frontend/src/utils/styleUtils.ts

The Innovative Form Builder – IvyForms, version 1.0. 564 lines.

- Page: https://pluginprobe.com/plugins/ivyforms/1.0/code/frontend/src/utils/styleUtils.ts
- Raw: https://pluginprobe.com/plugins/ivyforms/1.0/raw/frontend/src/utils/styleUtils.ts
- Modified: 2026-05-06T08:31:02+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ivyforms/1.0/code/frontend/src/utils/styleUtils.ts#L10-L20`.

```typescript
import type { Ref } from 'vue'
import { watch } from 'vue'
import type { FormStyleSettings } from '@/types/form-style'

/**
 * Represents a CSS dimension value split into numeric value and unit
 */
export interface DimensionValue {
  value: string
  unit: string
}

/**
 * Represents corner radius settings with linked/unlinked state
 */
export interface CornerSettings {
  isLinked: boolean
  value: string
  topLeft: string
  topRight: string
  bottomRight: string
  bottomLeft: string
}

/**
 * Form builder store interface for type safety
 */
export interface FormBuilderStore {
  styleSettings: FormStyleSettings
  updateStyleSettings: (path: string, value: unknown) => void
}

export type Alignment = 'left' | 'center' | 'right'
export type BorderStyle = 'solid' | 'dashed' | 'dotted'
export type FontStyle = 'normal' | 'italic'

const BORDER_STYLE_VALUES: BorderStyle[] = ['solid', 'dashed', 'dotted']
const FONT_STYLE_VALUES: FontStyle[] = ['normal', 'italic']

export const BORDER_STYLE_OPTIONS: Array<{ value: BorderStyle; label: string }> = [
  { value: 'solid', label: 'Solid' },
  { value: 'dashed', label: 'Dash' },
  { value: 'dotted', label: 'Dotted' },
]

export const FONT_STYLE_OPTIONS: Array<{ value: FontStyle; label: string }> = [
  { value: 'normal', label: 'Normal' },
  { value: 'italic', label: 'Italic' },
]

export function normalizeAlignment(value?: string): Alignment {
  if (value === 'center' || value === 'right') {
    return value as Alignment
  }
  return 'left'
}

export function resolveBorderStyle(
  borderStyle: unknown,
  fallbackValue: BorderStyle = 'solid',
): BorderStyle {
  if (typeof borderStyle === 'string' && BORDER_STYLE_VALUES.includes(borderStyle as BorderStyle)) {
    return borderStyle as BorderStyle
  }

  return fallbackValue
}

export function resolveFontStyle(
  fontStyle: unknown,
  fallbackValue: FontStyle = 'normal',
): FontStyle {
  if (typeof fontStyle === 'string' && FONT_STYLE_VALUES.includes(fontStyle as FontStyle)) {
    return fontStyle as FontStyle
  }

  return fallbackValue
}

export function getBorderStyleIcon(borderStyle: BorderStyle): string {
  if (borderStyle === 'dashed') {
    return 'border-dash'
  }
  if (borderStyle === 'dotted') {
    return 'border-dotted'
  }

  return 'border-solid'
}

/**
 * Configuration for creating a style property watcher
 */
export interface StyleWatcherConfig {
  ref: Ref<unknown>
  storePath: string
  syncPaths?: string[]
  formBuilderStore: FormBuilderStore
  isSyncingFromStore: Ref<boolean>
}

/**
 * Parse a CSS dimension string (e.g., "12px", "1.5rem") into value and unit
 * @param value - CSS dimension string
 * @returns Object with separated value and unit
 */
export function parseDimension(value: string): DimensionValue {
  const match = value?.match(/^([0-9.]+)(.*)$/)
  if (match) {
    return { value: match[1], unit: match[2] || 'px' }
  }
  return { value: '', unit: 'px' }
}

/**
 * Parse dimension and return numeric value with unit.
 * @param value - CSS dimension string
 * @param fallbackValue - Numeric fallback when parsed value is empty
 */
export function parseDimensionToNumeric(
  value: string,
  fallbackValue: number | null = null,
): { value: number | null; unit: string } {
  const parsedDimension = parseDimension(value)

  return {
    value: parsedDimension.value !== '' ? Number(parsedDimension.value) : fallbackValue,
    unit: parsedDimension.unit,
  }
}

/**
 * Resolve a style string value from store or fallback to the provided default.
 * Useful inside syncWithStore to avoid repeated `(storeVal || defaultVal) as string` casts.
 * @param storeValue - Value from the store (may be undefined)
 * @param defaultValue - Default value to use when storeValue is falsy
 */
export function resolveSyncStringValue(storeValue?: string, defaultValue?: string): string {
  return storeValue || defaultValue || ''
}

/**
 * Parse a dimension stored in the store, falling back to the default value.
 * Combines resolveSyncStringValue and parseDimensionToNumeric.
 * @param storeValue - CSS dimension string from store (may be undefined)
 * @param defaultValue - CSS dimension string from defaults (may be undefined)
 * @param fallbackValue - Numeric fallback when the resolved string cannot be parsed
 */
export function parseSyncNumericDimension(
  storeValue?: string,
  defaultValue?: string,
  fallbackValue: number | null = null,
): { value: number | null; unit: string } {
  return parseDimensionToNumeric(resolveSyncStringValue(storeValue, defaultValue), fallbackValue)
}

/**
 * Parse a dimension stored in the store, falling back to the default value.
 * Returns a raw { value, unit } string pair (not numeric).
 * @param storeValue - CSS dimension string from store (may be undefined)
 * @param defaultValue - CSS dimension string from defaults (may be undefined)
 */
export function parseSyncDimensionValue(
  storeValue?: string,
  defaultValue?: string,
): DimensionValue {
  return parseDimension(resolveSyncStringValue(storeValue, defaultValue))
}

/**
 * Extract the numeric corner value from a parsed DimensionValue.
 * Replaces the repeated `Number(parsed.value ?? fallback)` pattern at ref initialization.
 * Returns 0 when the parsed string value is empty.
 */
export function parseCornerNumericValue(parsed: DimensionValue): number {
  return parsed.value !== '' ? Number(parsed.value) : 0
}

/**
 * Resolve corner default value from either a plain dimension string
 * or a corner settings object containing a `value` field.
 */
export function resolveCornerDefaultValue(cornerSetting: unknown, fallbackValue = '0px'): string {
  if (typeof cornerSetting === 'string' && cornerSetting !== '') {
    return cornerSetting
  }

  if (cornerSetting && typeof cornerSetting === 'object') {
    const cornerObject = cornerSetting as Record<string, unknown>
    const cornerValue = cornerObject.value

    if (typeof cornerValue === 'string' && cornerValue !== '') {
      return cornerValue
    }
  }

  return fallbackValue
}

/**
 * Resolve corner linked state from corner settings object.
 */
export function resolveCornerLinkedState(cornerSetting: unknown, fallbackValue = true): boolean {
  if (cornerSetting && typeof cornerSetting === 'object') {
    const cornerObject = cornerSetting as Record<string, unknown>

    return cornerObject.isLinked !== false
  }

  return fallbackValue
}

/**
 * Build a CSS dimension string from value and unit
 * @param value - Numeric or string value
 * @param unit - CSS unit (px, rem, em, etc.)
 * @returns Formatted CSS dimension string or empty string if value is empty
 */
export function buildDimensionString(
  value: number | string | null | undefined,
  unit: string,
): string {
  if (value === '' || value === null || value === undefined) {
    return ''
  }
  return `${value}${unit}`
}

/**
 * Dimension input used by generators and formatters.
 * Accept either a CSS string (e.g. "16px"), or an object with numeric value and unit.
 */
export type DimensionSpec = string | { value: number | string | null; unit: string } | undefined

/**
 * Format a dimension spec into a CSS value string.
 * - If input is a string, returns it (except 'auto' returns empty string to indicate no explicit padding).
 * - If input is an object, returns `${value}${unit}` when value is not null/empty.
 * - Otherwise returns empty string.
 */
export function formatDimensionToCss(input: DimensionSpec): string {
  if (!input) return ''

  if (typeof input === 'string') {
    return input === 'auto' ? '' : input
  }

  if (input.value === null || input.value === '' || input.value === undefined) {
    return ''
  }

  return `${input.value}${input.unit || 'px'}`
}

/**
 * Apply corner settings from store data to component refs
 * @param cornerSetting - Corner data from store (can be string or object)
 * @param defaultValue - Fallback dimension string
 * @param linkedState - Ref for isLinked boolean
 * @param linkedValue - Ref for linked numeric value
 * @param linkedUnit - Ref for linked unit
 * @param topLeftValue - Ref for top-left numeric value
 * @param topLeftUnit - Ref for top-left unit
 * @param topRightValue - Ref for top-right numeric value
 * @param topRightUnit - Ref for top-right unit
 * @param bottomRightValue - Ref for bottom-right numeric value
 * @param bottomRightUnit - Ref for bottom-right unit
 * @param bottomLeftValue - Ref for bottom-left numeric value
 * @param bottomLeftUnit - Ref for bottom-left unit
 */
export function applyCornerSettings(
  cornerSetting: unknown,
  defaultValue: string,
  linkedState: Ref<boolean>,
  linkedValue: Ref<number>,
  linkedUnit: Ref<string>,
  topLeftValue: Ref<number>,
  topLeftUnit: Ref<string>,
  topRightValue: Ref<number>,
  topRightUnit: Ref<string>,
  bottomRightValue: Ref<number>,
  bottomRightUnit: Ref<string>,
  bottomLeftValue: Ref<number>,
  bottomLeftUnit: Ref<string>,
): void {
  // Handle string or missing corner setting - set all to linked
  if (!cornerSetting || typeof cornerSetting === 'string') {
    const normalizedValue = parseDimension((cornerSetting as string) || defaultValue)
    const defaultParsed = parseDimension(defaultValue)
    linkedState.value = true
    linkedValue.value =
      normalizedValue.value !== '' ? Number(normalizedValue.value) : Number(defaultParsed.value)
    linkedUnit.value = normalizedValue.unit
    topLeftValue.value = linkedValue.value
    topLeftUnit.value = linkedUnit.value
    topRightValue.value = linkedValue.value
    topRightUnit.value = linkedUnit.value
    bottomRightValue.value = linkedValue.value
    bottomRightUnit.value = linkedUnit.value
    bottomLeftValue.value = linkedValue.value
    bottomLeftUnit.value = linkedUnit.value
    return
  }

  // Handle object corner setting with individual corner values
  const cornerObject = cornerSetting as Record<string, unknown>
  const isLinkedValue = cornerObject.isLinked !== false
  const linkedValueRaw = (cornerObject.value as string) || defaultValue
  const linkedValueData = parseDimension(linkedValueRaw)
  const defaultParsed = parseDimension(defaultValue)
  const topLeftValueData = parseDimension((cornerObject.topLeft as string) || linkedValueRaw)
  const topRightValueData = parseDimension((cornerObject.topRight as string) || linkedValueRaw)
  const bottomRightValueData = parseDimension(
    (cornerObject.bottomRight as string) || linkedValueRaw,
  )
  const bottomLeftValueData = parseDimension((cornerObject.bottomLeft as string) || linkedValueRaw)

  linkedState.value = isLinkedValue
  linkedValue.value =
    linkedValueData.value !== '' ? Number(linkedValueData.value) : Number(defaultParsed.value)
  linkedUnit.value = linkedValueData.unit

  const linkedFallback = linkedValue.value
  topLeftValue.value =
    topLeftValueData.value !== '' ? Number(topLeftValueData.value) : linkedFallback
  topLeftUnit.value = topLeftValueData.unit
  topRightValue.value =
    topRightValueData.value !== '' ? Number(topRightValueData.value) : linkedFallback
  topRightUnit.value = topRightValueData.unit
  bottomRightValue.value =
    bottomRightValueData.value !== '' ? Number(bottomRightValueData.value) : linkedFallback
  bottomRightUnit.value = bottomRightValueData.unit
  bottomLeftValue.value =
    bottomLeftValueData.value !== '' ? Number(bottomLeftValueData.value) : linkedFallback
  bottomLeftUnit.value = bottomLeftValueData.unit
}

/**
 * Build corner settings object from component refs for storing
 * @param isLinked - Whether corners are linked
 * @param linkedValue - Linked corner value
 * @param linkedUnit - Linked corner unit
 * @param topLeftValue - Top-left corner value
 * @param topLeftUnit - Top-left corner unit
 * @param topRightValue - Top-right corner value
 * @param topRightUnit - Top-right corner unit
 * @param bottomRightValue - Bottom-right corner value
 * @param bottomRightUnit - Bottom-right corner unit
 * @param bottomLeftValue - Bottom-left corner value
 * @param bottomLeftUnit - Bottom-left corner unit
 * @returns CornerSettings object ready for store
 */
export function buildCornerSettings(
  isLinked: boolean,
  linkedValue: number,
  linkedUnit: string,
  topLeftValue: number,
  topLeftUnit: string,
  topRightValue: number,
  topRightUnit: string,
  bottomRightValue: number,
  bottomRightUnit: string,
  bottomLeftValue: number,
  bottomLeftUnit: string,
): CornerSettings {
  return {
    isLinked,
    value: buildDimensionString(linkedValue, linkedUnit),
    topLeft: buildDimensionString(topLeftValue, topLeftUnit),
    topRight: buildDimensionString(topRightValue, topRightUnit),
    bottomRight: buildDimensionString(bottomRightValue, bottomRightUnit),
    bottomLeft: buildDimensionString(bottomLeftValue, bottomLeftUnit),
  }
}

/**
 * Toggle corner linked state and synchronize values
 * @param isLinked - Current linked state ref
 * @param linkedValue - Linked value ref
 * @param linkedUnit - Linked unit ref
 * @param topLeftValue - Top-left value ref
 * @param topLeftUnit - Top-left unit ref
 * @param topRightValue - Top-right value ref
 * @param topRightUnit - Top-right unit ref
 * @param bottomRightValue - Bottom-right value ref
 * @param bottomRightUnit - Bottom-right unit ref
 * @param bottomLeftValue - Bottom-left value ref
 * @param bottomLeftUnit - Bottom-left unit ref
 */
export function toggleCornerLinked(
  isLinked: Ref<boolean>,
  linkedValue: Ref<number>,
  linkedUnit: Ref<string>,
  topLeftValue: Ref<number>,
  topLeftUnit: Ref<string>,
  topRightValue: Ref<number>,
  topRightUnit: Ref<string>,
  bottomRightValue: Ref<number>,
  bottomRightUnit: Ref<string>,
  bottomLeftValue: Ref<number>,
  bottomLeftUnit: Ref<string>,
): void {
  if (isLinked.value) {
    // Unlinking: copy linked values to individual corners
    isLinked.value = false
    topLeftValue.value = linkedValue.value
    topRightValue.value = linkedValue.value
    bottomRightValue.value = linkedValue.value
    bottomLeftValue.value = linkedValue.value
    topLeftUnit.value = linkedUnit.value
    topRightUnit.value = linkedUnit.value
    bottomRightUnit.value = linkedUnit.value
    bottomLeftUnit.value = linkedUnit.value
  } else {
    // Linking: copy top-left to linked value
    isLinked.value = true
    linkedValue.value = topLeftValue.value ?? linkedValue.value
    linkedUnit.value = topLeftUnit.value ?? linkedUnit.value
  }
}

/**
 * Safely extract a style value from store with fallback chain
 * @param storeValue - Value from store (may be undefined)
 * @param defaultValue - Default theme value (may be undefined)
 * @param fallback - Final fallback value
 * @returns The first non-undefined value in the chain, or fallback
 */
export function syncStyleValue<T>(
  storeValue: T | undefined,
  defaultValue: T | undefined,
  fallback: T,
): T {
  return storeValue ?? defaultValue ?? fallback
}

/**
 * Get nested property value from object using dot notation path
 * @param obj - Source object
 * @param path - Dot-separated path (e.g., 'advancedMode.general.formBackground')
 * @returns Value at path or undefined
 */
export function getNestedValue(
  obj: FormStyleSettings | Record<string, unknown>,
  path: string,
): unknown {
  return path.split('.').reduce((current: unknown, key: string) => {
    return current && typeof current === 'object' && key in current
      ? (current as Record<string, unknown>)[key]
      : undefined
  }, obj)
}

/**
 * Update multiple store paths with the same value
 * Useful for syncing Quick mode and Advanced mode simultaneously
 * @param formBuilderStore - The form builder store instance
 * @param paths - Array of dot-notation paths to update
 * @param value - Value to set for all paths
 */
export function updateStylePaths(
  formBuilderStore: FormBuilderStore,
  paths: string[],
  value: unknown,
): void {
  paths.forEach((path) => {
    formBuilderStore.updateStyleSettings(path, value)
  })
}

/**
 * Create a watcher for a style property that automatically syncs with store
 * Handles the common pattern of checking sync state, comparing values, and updating store
 * @param config - Watcher configuration object
 * @returns Cleanup function to stop the watcher
 */
export function createStyleWatcher(config: StyleWatcherConfig): () => void {
  const { ref: watchRef, storePath, syncPaths, formBuilderStore, isSyncingFromStore } = config

  let timeoutId: ReturnType<typeof setTimeout> | undefined

  const stop = watch(watchRef, (newValue) => {
    if (isSyncingFromStore.value) return

    if (timeoutId) {
      clearTimeout(timeoutId)
    }

    timeoutId = setTimeout(() => {
      const currentValue = getNestedValue(formBuilderStore.styleSettings, storePath)

      if (newValue !== currentValue) {
        // If syncPaths provided, update multiple paths; otherwise just the storePath
        if (syncPaths && syncPaths.length > 0) {
          updateStylePaths(formBuilderStore, syncPaths, newValue)
        } else {
          formBuilderStore.updateStyleSettings(storePath, newValue)
        }
      }

      timeoutId = undefined
    }, 0)
  })

  return () => {
    if (timeoutId) {
      clearTimeout(timeoutId)
      timeoutId = undefined
    }

    stop()
  }
}

/**
 * Create watchers for dimension value+unit pairs (e.g., borderWidth)
 * Handles the pattern of watching both value and unit refs together
 * @param valueRef - Ref containing numeric value
 * @param unitRef - Ref containing unit string
 * @param storePath - Primary store path to compare against
 * @param syncPaths - Array of paths to update (for dual-mode sync)
 * @param formBuilderStore - Form builder store instance
 * @param isSyncingFromStore - Flag to prevent circular updates
 * @returns Cleanup function to stop the watcher
 */
export function createDimensionWatcher(
  valueRef: Ref<number>,
  unitRef: Ref<string>,
  storePath: string,
  syncPaths: string[],
  formBuilderStore: FormBuilderStore,
  isSyncingFromStore: Ref<boolean>,
): () => void {
  let timeoutId: ReturnType<typeof setTimeout> | undefined

  const stop = watch([valueRef, unitRef], () => {
    if (isSyncingFromStore.value) return

    if (timeoutId) {
      clearTimeout(timeoutId)
    }

    timeoutId = setTimeout(() => {
      const currentValue = getNestedValue(formBuilderStore.styleSettings, storePath)
      const newValue = buildDimensionString(valueRef.value, unitRef.value)

      if (newValue !== '' && newValue !== currentValue) {
        updateStylePaths(formBuilderStore, syncPaths, newValue)
      }

      timeoutId = undefined
    }, 0)
  })

  return () => {
    if (timeoutId) {
      clearTimeout(timeoutId)
      timeoutId = undefined
    }

    stop()
  }
}

```
