- Introduced a new 'Page Wrapper' node type to allow multiple components (forms, HTML, tables) to be rendered as a single page, enhancing the flexibility of the process builder. - Implemented child node management within the Page Wrapper, including adding, updating, and removing child nodes through the process store. - Enhanced the ProcessFlowCanvas to support the new Page Wrapper node, including event handling for child node interactions and configuration. - Updated the workflow page to handle submissions and interactions for Page Wrapper nodes, ensuring seamless integration with existing process flows. - Improved documentation in CLAUDE.md to provide guidance on the new Page Wrapper architecture and its components. - Refactored related components and store logic to accommodate the new functionality, ensuring a cohesive user experience across the application.
882 lines
26 KiB
Vue
882 lines
26 KiB
Vue
<template>
|
|
<div class="page-wrapper-configuration">
|
|
<!-- Page Information -->
|
|
<div class="mb-6">
|
|
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
|
<Icon name="material-symbols:info-outline" class="w-4 h-4 mr-2 text-blue-500" />
|
|
Page Information
|
|
</h4>
|
|
|
|
<!-- Page Title -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Page Title</label>
|
|
<input
|
|
type="text"
|
|
v-model="localData.title"
|
|
placeholder="e.g., Customer Registration, Dashboard, Settings"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
@input="emitUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Description
|
|
<span class="text-xs text-gray-500">(Optional)</span>
|
|
</label>
|
|
<textarea
|
|
v-model="localData.description"
|
|
placeholder="Brief description of what this page does..."
|
|
rows="2"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
@input="emitUpdate"
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layout Selection -->
|
|
<div class="mb-6">
|
|
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
|
<Icon name="material-symbols:view-module" class="w-4 h-4 mr-2 text-blue-500" />
|
|
How should components be arranged?
|
|
</h4>
|
|
|
|
<!-- Visual Layout Selection -->
|
|
<div class="grid grid-cols-1 gap-3">
|
|
<!-- Grid Layout -->
|
|
<div
|
|
:class="[
|
|
'layout-option',
|
|
localData.layout === 'grid' ? 'selected' : ''
|
|
]"
|
|
@click="selectLayout('grid')"
|
|
>
|
|
<div class="layout-preview">
|
|
<div class="grid-preview">
|
|
<div class="preview-item"></div>
|
|
<div class="preview-item"></div>
|
|
<div class="preview-item"></div>
|
|
<div class="preview-item"></div>
|
|
</div>
|
|
</div>
|
|
<div class="layout-info">
|
|
<h5 class="font-medium text-gray-800">Organized Rows & Columns</h5>
|
|
<p class="text-xs text-gray-600">Perfect for forms, cards, or structured content</p>
|
|
</div>
|
|
<div class="layout-radio">
|
|
<input
|
|
type="radio"
|
|
:checked="localData.layout === 'grid'"
|
|
class="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
readonly
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Flex Layout -->
|
|
<div
|
|
:class="[
|
|
'layout-option',
|
|
localData.layout === 'flex' ? 'selected' : ''
|
|
]"
|
|
@click="selectLayout('flex')"
|
|
>
|
|
<div class="layout-preview">
|
|
<div class="flex-preview">
|
|
<div class="preview-item"></div>
|
|
<div class="preview-item"></div>
|
|
<div class="preview-item"></div>
|
|
</div>
|
|
</div>
|
|
<div class="layout-info">
|
|
<h5 class="font-medium text-gray-800">Side by Side</h5>
|
|
<p class="text-xs text-gray-600">Components arranged horizontally, responsive</p>
|
|
</div>
|
|
<div class="layout-radio">
|
|
<input
|
|
type="radio"
|
|
:checked="localData.layout === 'flex'"
|
|
class="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
readonly
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stacked Layout -->
|
|
<div
|
|
:class="[
|
|
'layout-option',
|
|
localData.layout === 'stacked' ? 'selected' : ''
|
|
]"
|
|
@click="selectLayout('stacked')"
|
|
>
|
|
<div class="layout-preview">
|
|
<div class="stacked-preview">
|
|
<div class="preview-item"></div>
|
|
<div class="preview-item"></div>
|
|
<div class="preview-item"></div>
|
|
</div>
|
|
</div>
|
|
<div class="layout-info">
|
|
<h5 class="font-medium text-gray-800">One Below Another</h5>
|
|
<p class="text-xs text-gray-600">Components stacked vertically, great for mobile</p>
|
|
</div>
|
|
<div class="layout-radio">
|
|
<input
|
|
type="radio"
|
|
:checked="localData.layout === 'stacked'"
|
|
class="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
readonly
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layout-Specific Settings -->
|
|
<div class="mb-6" v-if="localData.layout">
|
|
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
|
<Icon name="material-symbols:tune" class="w-4 h-4 mr-2 text-blue-500" />
|
|
Layout Settings
|
|
</h4>
|
|
|
|
<!-- Grid Columns (only for grid layout) -->
|
|
<div v-if="localData.layout === 'grid'" class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
How many columns?
|
|
<span class="text-xs text-gray-500">(1-6 columns)</span>
|
|
</label>
|
|
<div class="grid grid-cols-6 gap-2">
|
|
<button
|
|
v-for="num in 6"
|
|
:key="num"
|
|
:class="[
|
|
'column-selector',
|
|
localData.columns === num ? 'selected' : ''
|
|
]"
|
|
@click="selectColumns(num)"
|
|
>
|
|
{{ num }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Component Spacing -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Space between components
|
|
<span class="text-xs text-gray-500">(How much gap between each component)</span>
|
|
</label>
|
|
<div class="spacing-options">
|
|
<div
|
|
v-for="option in spacingOptions"
|
|
:key="option.value"
|
|
:class="[
|
|
'spacing-option',
|
|
localData.gap === option.value ? 'selected' : ''
|
|
]"
|
|
@click="selectSpacing(option.value)"
|
|
>
|
|
<div class="spacing-preview" :style="{ gap: option.preview }">
|
|
<div class="spacing-item"></div>
|
|
<div class="spacing-item"></div>
|
|
</div>
|
|
<span class="spacing-label">{{ option.label }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Container Padding -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
Space around content
|
|
<span class="text-xs text-gray-500">(Padding inside the container)</span>
|
|
</label>
|
|
<div class="spacing-options">
|
|
<div
|
|
v-for="option in paddingOptions"
|
|
:key="option.value"
|
|
:class="[
|
|
'spacing-option',
|
|
localData.padding === option.value ? 'selected' : ''
|
|
]"
|
|
@click="selectPadding(option.value)"
|
|
>
|
|
<div class="padding-preview">
|
|
<div class="padding-container" :style="{ padding: option.preview }">
|
|
<div class="padding-content"></div>
|
|
</div>
|
|
</div>
|
|
<span class="spacing-label">{{ option.label }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Components in this Page -->
|
|
<div class="mb-6">
|
|
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
|
<Icon name="material-symbols:widgets" class="w-4 h-4 mr-2 text-blue-500" />
|
|
Components in this Page
|
|
</h4>
|
|
|
|
<!-- Drag and Drop Instruction -->
|
|
<div v-if="!localData.childNodes || localData.childNodes.length === 0" class="drag-instruction">
|
|
<div class="drag-drop-area">
|
|
<Icon name="material-symbols:drag-indicator" class="w-8 h-8 text-gray-300 mb-2" />
|
|
<h5 class="font-medium text-gray-600 mb-1">Add Components</h5>
|
|
<p class="text-sm text-gray-500 mb-3">
|
|
Drag <strong>Form</strong>, <strong>HTML</strong>, or <strong>Table</strong> nodes from the component palette into the PageWrapper container on the canvas.
|
|
</p>
|
|
<div class="flex items-center justify-center space-x-4 text-xs text-gray-400">
|
|
<div class="flex items-center">
|
|
<Icon name="material-symbols:description-outline" class="w-4 h-4 mr-1" />
|
|
Form
|
|
</div>
|
|
<div class="flex items-center">
|
|
<Icon name="material-symbols:code" class="w-4 h-4 mr-1" />
|
|
HTML
|
|
</div>
|
|
<div class="flex items-center">
|
|
<Icon name="material-symbols:table" class="w-4 h-4 mr-1" />
|
|
Table
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Component List -->
|
|
<div v-else class="space-y-3">
|
|
<div class="component-list-header">
|
|
<p class="text-sm text-gray-600 mb-3">
|
|
<Icon name="material-symbols:check-circle" class="w-4 h-4 mr-1 text-green-500 inline" />
|
|
{{ localData.childNodes.length }} component{{ localData.childNodes.length !== 1 ? 's' : '' }} added
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
v-for="(childNode, index) in localData.childNodes"
|
|
:key="childNode.id || index"
|
|
class="component-item"
|
|
>
|
|
<div class="component-header">
|
|
<div class="component-info">
|
|
<Icon :name="getChildNodeIcon(childNode.type)" class="w-5 h-5 mr-3 text-blue-500" />
|
|
<div>
|
|
<h5 class="font-medium text-gray-800">{{ getChildNodeTypeName(childNode.type) }}</h5>
|
|
<p class="text-xs text-gray-500">{{ childNode.nodeId || 'No ID specified' }}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
@click="removeChildNode(index)"
|
|
class="remove-btn"
|
|
title="Remove component"
|
|
>
|
|
<Icon name="material-symbols:close" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Component Settings -->
|
|
<div class="component-settings" v-if="showComponentSettings[index]">
|
|
<div class="settings-grid">
|
|
<!-- Component ID -->
|
|
<div class="setting-item">
|
|
<label class="setting-label">Component Reference</label>
|
|
<input
|
|
type="text"
|
|
v-model="childNode.nodeId"
|
|
placeholder="e.g., user-form, contact-info"
|
|
class="setting-input"
|
|
@input="emitUpdate"
|
|
/>
|
|
<p class="setting-help">Unique identifier for this component</p>
|
|
</div>
|
|
|
|
<!-- Grid Position (for grid layout) -->
|
|
<div v-if="localData.layout === 'grid'" class="setting-item col-span-2">
|
|
<label class="setting-label">Position in Grid</label>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<input
|
|
type="number"
|
|
v-model.number="childNode.position.row"
|
|
placeholder="Row"
|
|
min="1"
|
|
class="setting-input"
|
|
@input="emitUpdate"
|
|
/>
|
|
<p class="text-xs text-gray-500">Row</p>
|
|
</div>
|
|
<div>
|
|
<input
|
|
type="number"
|
|
v-model.number="childNode.position.col"
|
|
placeholder="Column"
|
|
min="1"
|
|
:max="localData.columns"
|
|
class="setting-input"
|
|
@input="emitUpdate"
|
|
/>
|
|
<p class="text-xs text-gray-500">Column</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conditional Logic -->
|
|
<div class="setting-item col-span-2">
|
|
<div class="flex items-center mb-2">
|
|
<input
|
|
type="checkbox"
|
|
:id="`conditional-${index}`"
|
|
v-model="childNode.conditionalLogic.enabled"
|
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
@change="emitUpdate"
|
|
/>
|
|
<label :for="`conditional-${index}`" class="ml-2 block text-sm font-medium text-gray-600">
|
|
Show/hide based on conditions
|
|
</label>
|
|
</div>
|
|
|
|
<div v-if="childNode.conditionalLogic.enabled" class="conditional-settings">
|
|
<p class="text-xs text-gray-500 mb-3">Show this component when:</p>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Variable</label>
|
|
<VariableBrowser
|
|
v-model="childNode.conditionalLogic.variable"
|
|
:availableVariables="availableVariables"
|
|
placeholder="Select variable"
|
|
:allowCreate="false"
|
|
@update:modelValue="emitUpdate"
|
|
/>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Condition</label>
|
|
<select
|
|
v-model="childNode.conditionalLogic.operator"
|
|
class="setting-input text-xs"
|
|
@change="emitUpdate"
|
|
>
|
|
<option value="==">equals</option>
|
|
<option value="!=">doesn't equal</option>
|
|
<option value=">">is greater than</option>
|
|
<option value="<">is less than</option>
|
|
<option value=">=">greater or equal</option>
|
|
<option value="<=">less or equal</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Value</label>
|
|
<input
|
|
type="text"
|
|
v-model="childNode.conditionalLogic.value"
|
|
placeholder="Enter value"
|
|
class="setting-input text-xs"
|
|
@input="emitUpdate"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Toggle -->
|
|
<button
|
|
@click="toggleComponentSettings(index)"
|
|
class="settings-toggle"
|
|
>
|
|
<Icon
|
|
:name="showComponentSettings[index] ? 'material-symbols:expand-less' : 'material-symbols:expand-more'"
|
|
class="w-4 h-4 mr-1"
|
|
/>
|
|
{{ showComponentSettings[index] ? 'Hide Settings' : 'Show Settings' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Options -->
|
|
<div class="mb-6">
|
|
<button
|
|
@click="showAdvancedOptions = !showAdvancedOptions"
|
|
class="advanced-toggle"
|
|
>
|
|
<Icon name="material-symbols:settings" class="w-4 h-4 mr-2" />
|
|
<span>Advanced Options</span>
|
|
<Icon
|
|
:name="showAdvancedOptions ? 'material-symbols:expand-less' : 'material-symbols:expand-more'"
|
|
class="w-4 h-4 ml-2"
|
|
/>
|
|
</button>
|
|
|
|
<div v-if="showAdvancedOptions" class="advanced-options">
|
|
<!-- Background Color -->
|
|
<div class="advanced-setting">
|
|
<label class="advanced-label">Background Color</label>
|
|
<div class="color-picker-container">
|
|
<input
|
|
type="color"
|
|
v-model="localData.backgroundColor"
|
|
class="color-picker"
|
|
@input="emitUpdate"
|
|
/>
|
|
<span class="color-value">{{ localData.backgroundColor }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom CSS -->
|
|
<div class="advanced-setting">
|
|
<label class="advanced-label">Custom CSS Styling</label>
|
|
<textarea
|
|
v-model="localData.customCSS"
|
|
placeholder="/* Add custom CSS styles here */ .my-custom-class { margin: 10px; padding: 20px; }"
|
|
rows="4"
|
|
class="code-textarea"
|
|
@input="emitUpdate"
|
|
></textarea>
|
|
<p class="advanced-help">Add custom CSS to style your page components</p>
|
|
</div>
|
|
|
|
<!-- Custom JavaScript -->
|
|
<div class="advanced-setting">
|
|
<label class="advanced-label">Custom JavaScript</label>
|
|
<textarea
|
|
v-model="localData.customJS"
|
|
placeholder="// Add custom JavaScript here console.log('Page loaded'); // Access process variables // const userRole = processVariables.userRole;"
|
|
rows="4"
|
|
class="code-textarea"
|
|
@input="emitUpdate"
|
|
></textarea>
|
|
<p class="advanced-help">Add custom JavaScript for dynamic behavior</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from 'vue'
|
|
import { Icon } from '#components'
|
|
import VariableBrowser from './VariableBrowser.vue'
|
|
|
|
const props = defineProps({
|
|
nodeData: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
availableVariables: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update'])
|
|
|
|
// Reactive data
|
|
const showAdvancedOptions = ref(false)
|
|
const showComponentSettings = ref({})
|
|
|
|
// Local data copy to prevent direct prop mutation
|
|
const localData = ref({
|
|
title: 'Multi-Component Page',
|
|
description: 'Multi-component page container',
|
|
layout: 'grid',
|
|
columns: 2,
|
|
gap: '1rem',
|
|
padding: '1rem',
|
|
childNodes: [],
|
|
backgroundColor: '#ffffff',
|
|
customCSS: '',
|
|
customJS: '',
|
|
pageVariables: {},
|
|
...props.nodeData
|
|
})
|
|
|
|
// Spacing and padding options for visual selection
|
|
const spacingOptions = [
|
|
{ label: 'None', value: '0', preview: '0px' },
|
|
{ label: 'Small', value: '0.5rem', preview: '4px' },
|
|
{ label: 'Medium', value: '1rem', preview: '8px' },
|
|
{ label: 'Large', value: '1.5rem', preview: '12px' },
|
|
{ label: 'Extra Large', value: '2rem', preview: '16px' }
|
|
]
|
|
|
|
const paddingOptions = [
|
|
{ label: 'None', value: '0', preview: '0px' },
|
|
{ label: 'Small', value: '0.5rem', preview: '2px' },
|
|
{ label: 'Medium', value: '1rem', preview: '4px' },
|
|
{ label: 'Large', value: '1.5rem', preview: '6px' },
|
|
{ label: 'Extra Large', value: '2rem', preview: '8px' }
|
|
]
|
|
|
|
// Layout selection methods
|
|
const selectLayout = (layoutType) => {
|
|
localData.value.layout = layoutType
|
|
// Set smart defaults based on layout
|
|
if (layoutType === 'grid') {
|
|
localData.value.columns = localData.value.columns || 2
|
|
}
|
|
emitUpdate()
|
|
}
|
|
|
|
const selectColumns = (numColumns) => {
|
|
localData.value.columns = numColumns
|
|
emitUpdate()
|
|
}
|
|
|
|
const selectSpacing = (spacing) => {
|
|
localData.value.gap = spacing
|
|
emitUpdate()
|
|
}
|
|
|
|
const selectPadding = (padding) => {
|
|
localData.value.padding = padding
|
|
emitUpdate()
|
|
}
|
|
|
|
// Component settings management
|
|
const toggleComponentSettings = (index) => {
|
|
showComponentSettings.value[index] = !showComponentSettings.value[index]
|
|
}
|
|
|
|
// Generate unique ID for child nodes
|
|
const generateChildNodeId = () => {
|
|
return `child-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
}
|
|
|
|
// Remove child node
|
|
const removeChildNode = (index) => {
|
|
localData.value.childNodes.splice(index, 1)
|
|
// Clean up settings state
|
|
delete showComponentSettings.value[index]
|
|
emitUpdate()
|
|
}
|
|
|
|
// Get child node type icon
|
|
const getChildNodeIcon = (type) => {
|
|
const iconMap = {
|
|
form: 'material-symbols:description-outline',
|
|
html: 'material-symbols:code',
|
|
table: 'material-symbols:table'
|
|
}
|
|
return iconMap[type] || 'material-symbols:extension'
|
|
}
|
|
|
|
// Get child node type display name
|
|
const getChildNodeTypeName = (type) => {
|
|
const nameMap = {
|
|
form: 'Form Component',
|
|
html: 'HTML Content',
|
|
table: 'Table Component'
|
|
}
|
|
return nameMap[type] || 'Unknown Component'
|
|
}
|
|
|
|
// Emit update event
|
|
const emitUpdate = () => {
|
|
emit('update', { ...localData.value })
|
|
}
|
|
|
|
// Initialize child nodes with proper structure
|
|
const initializeChildNodes = () => {
|
|
if (localData.value.childNodes) {
|
|
localData.value.childNodes = localData.value.childNodes.map(child => ({
|
|
...child,
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
variable: '',
|
|
operator: '==',
|
|
value: '',
|
|
...child.conditionalLogic
|
|
},
|
|
position: {
|
|
row: 1,
|
|
col: 1,
|
|
...child.position
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
// Watch for prop changes
|
|
watch(() => props.nodeData, (newData) => {
|
|
localData.value = { ...localData.value, ...newData }
|
|
initializeChildNodes()
|
|
}, { deep: true, immediate: true })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page-wrapper-configuration {
|
|
/* max-height: 600px; */
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
.page-wrapper-configuration::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.page-wrapper-configuration::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.page-wrapper-configuration::-webkit-scrollbar-thumb {
|
|
background: #c1c1c1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.page-wrapper-configuration::-webkit-scrollbar-thumb:hover {
|
|
background: #a1a1a1;
|
|
}
|
|
|
|
/* Layout Selection Styles */
|
|
.layout-option {
|
|
@apply border border-gray-200 rounded-lg p-4 cursor-pointer transition-all duration-200 bg-white hover:border-blue-300 hover:shadow-sm;
|
|
}
|
|
|
|
.layout-option.selected {
|
|
@apply border-blue-500 bg-blue-50 shadow-sm;
|
|
}
|
|
|
|
.layout-option {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.layout-preview {
|
|
width: 48px;
|
|
height: 32px;
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.grid-preview {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 2px;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.flex-preview {
|
|
display: flex;
|
|
gap: 2px;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.stacked-preview {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.preview-item {
|
|
background: #3b82f6;
|
|
border-radius: 1px;
|
|
min-height: 6px;
|
|
flex: 1;
|
|
}
|
|
|
|
.layout-info h5 {
|
|
margin: 0 0 2px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.layout-info p {
|
|
margin: 0;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.layout-radio input {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Column Selector */
|
|
.column-selector {
|
|
@apply w-10 h-10 border border-gray-300 rounded-md flex items-center justify-center text-sm font-medium transition-all duration-200 bg-white hover:border-blue-400;
|
|
}
|
|
|
|
.column-selector.selected {
|
|
@apply bg-blue-500 text-white border-blue-500;
|
|
}
|
|
|
|
/* Spacing Options */
|
|
.spacing-options {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
|
gap: 8px;
|
|
}
|
|
|
|
.spacing-option {
|
|
@apply border border-gray-200 rounded-lg p-3 cursor-pointer transition-all duration-200 bg-white hover:border-blue-300 text-center;
|
|
}
|
|
|
|
.spacing-option.selected {
|
|
@apply border-blue-500 bg-blue-50;
|
|
}
|
|
|
|
.spacing-preview {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 20px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.spacing-item {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #94a3b8;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.padding-preview {
|
|
height: 20px;
|
|
margin-bottom: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.padding-container {
|
|
border: 1px solid #94a3b8;
|
|
border-radius: 2px;
|
|
width: 20px;
|
|
height: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.padding-content {
|
|
width: 8px;
|
|
height: 8px;
|
|
background: #94a3b8;
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.spacing-label {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: #4b5563;
|
|
}
|
|
|
|
/* Drag Instruction */
|
|
.drag-instruction {
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.drag-drop-area {
|
|
@apply border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-gray-50;
|
|
}
|
|
|
|
/* Component List */
|
|
.component-item {
|
|
@apply border border-gray-200 rounded-lg bg-white transition-all duration-200;
|
|
}
|
|
|
|
.component-header {
|
|
@apply p-4 flex items-center justify-between;
|
|
}
|
|
|
|
.component-info {
|
|
@apply flex items-center flex-1;
|
|
}
|
|
|
|
.component-info h5 {
|
|
@apply text-sm font-medium text-gray-800 m-0;
|
|
}
|
|
|
|
.component-info p {
|
|
@apply text-xs text-gray-500 m-0;
|
|
}
|
|
|
|
.remove-btn {
|
|
@apply w-8 h-8 flex items-center justify-center rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-all duration-200;
|
|
}
|
|
|
|
/* Component Settings */
|
|
.component-settings {
|
|
@apply border-t border-gray-100 p-4 bg-gray-50;
|
|
}
|
|
|
|
.settings-grid {
|
|
@apply grid grid-cols-2 gap-4;
|
|
}
|
|
|
|
.setting-item {
|
|
@apply space-y-2;
|
|
}
|
|
|
|
.setting-label {
|
|
@apply block text-sm font-medium text-gray-700;
|
|
}
|
|
|
|
.setting-input {
|
|
@apply w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
|
|
}
|
|
|
|
.setting-help {
|
|
@apply text-xs text-gray-500 m-0;
|
|
}
|
|
|
|
.conditional-settings {
|
|
@apply p-4 bg-white border border-gray-200 rounded-md mt-2;
|
|
}
|
|
|
|
.settings-toggle {
|
|
@apply w-full p-2 text-sm text-gray-600 hover:text-blue-600 hover:bg-blue-50 transition-all duration-200 flex items-center justify-center border-t border-gray-100;
|
|
}
|
|
|
|
/* Advanced Options */
|
|
.advanced-toggle {
|
|
@apply w-full p-3 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium text-gray-700 flex items-center justify-center transition-all duration-200;
|
|
}
|
|
|
|
.advanced-options {
|
|
@apply mt-4 space-y-4 p-4 bg-gray-50 rounded-lg border border-gray-200;
|
|
}
|
|
|
|
.advanced-setting {
|
|
@apply space-y-2;
|
|
}
|
|
|
|
.advanced-label {
|
|
@apply block text-sm font-medium text-gray-700;
|
|
}
|
|
|
|
.color-picker-container {
|
|
@apply flex items-center space-x-3;
|
|
}
|
|
|
|
.color-picker {
|
|
@apply w-12 h-10 border border-gray-300 rounded-md cursor-pointer;
|
|
}
|
|
|
|
.color-value {
|
|
@apply text-sm font-mono text-gray-600;
|
|
}
|
|
|
|
.code-textarea {
|
|
@apply w-full px-3 py-2 border border-gray-300 rounded-md text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-900 text-green-400;
|
|
}
|
|
|
|
.advanced-help {
|
|
@apply text-xs text-gray-500 m-0;
|
|
}
|
|
</style> |