- Updated FormBuilderComponents.vue to introduce an enhanced search bar with filtering options for components, improving user experience in locating specific items. - Added a filter dropdown to allow users to filter components by category, with clear options for selection and removal of filters. - Implemented recently used components section to provide quick access to frequently utilized items, enhancing workflow efficiency. - Enhanced search functionality to highlight matching components and provide feedback when no results are found, ensuring better usability. - Updated styles for improved visual consistency and responsiveness across different screen sizes. - Modified settings.local.json to include ESLint command in the allowed commands list for better development practices.
2201 lines
62 KiB
Vue
2201 lines
62 KiB
Vue
<template>
|
|
<div class="form-components">
|
|
<!-- Enhanced Search Bar -->
|
|
<div class="search-container p-3 mb-2">
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
v-model="searchQuery"
|
|
placeholder="Search components..."
|
|
class="w-full px-3 py-2 pl-9 pr-10 bg-white border border-gray-300 rounded text-gray-700 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
@focus="showSearchOptions = true"
|
|
/>
|
|
<Icon
|
|
name="material-symbols:search"
|
|
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"
|
|
/>
|
|
<button
|
|
@click="toggleFilterDropdown"
|
|
class="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 hover:bg-gray-100 rounded transition-colors"
|
|
:class="{ 'text-blue-500': hasActiveFilters }"
|
|
>
|
|
<Icon name="material-symbols:filter-list" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filter Dropdown -->
|
|
<div
|
|
v-if="showFilterDropdown"
|
|
class="absolute z-50 mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg"
|
|
>
|
|
<div class="p-3">
|
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Filter by Category</h4>
|
|
<div class="space-y-2 max-h-48 overflow-y-auto">
|
|
<label
|
|
v-for="category in availableCategories"
|
|
:key="category"
|
|
class="flex items-center space-x-2 cursor-pointer hover:bg-gray-50 p-1 rounded"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:value="category"
|
|
v-model="selectedCategories"
|
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
/>
|
|
<span class="text-sm text-gray-600">{{ category }}</span>
|
|
<span class="text-xs text-gray-400 ml-auto">
|
|
({{ getComponentsByCategory(category).length }})
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div class="flex justify-between mt-3 pt-2 border-t border-gray-100">
|
|
<button
|
|
@click="clearFilters"
|
|
class="text-xs text-gray-500 hover:text-gray-700"
|
|
>
|
|
Clear all
|
|
</button>
|
|
<button
|
|
@click="showFilterDropdown = false"
|
|
class="text-xs text-blue-600 hover:text-blue-700"
|
|
>
|
|
Done
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category Filter Chips -->
|
|
<div v-if="selectedCategories.length > 0" class="px-3 mb-3">
|
|
<div class="flex flex-wrap gap-1">
|
|
<span
|
|
v-for="category in selectedCategories"
|
|
:key="category"
|
|
class="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
|
|
>
|
|
{{ category }}
|
|
<button
|
|
@click="removeCategory(category)"
|
|
class="ml-1 hover:bg-blue-200 rounded-full p-0.5"
|
|
>
|
|
<Icon name="material-symbols:close" class="w-3 h-3" />
|
|
</button>
|
|
</span>
|
|
<button
|
|
@click="clearFilters"
|
|
class="text-xs text-gray-500 hover:text-gray-700 ml-1"
|
|
>
|
|
Clear all
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recently Used Section -->
|
|
<div v-if="recentlyUsed.length > 0 && !searchQuery && selectedCategories.length === 0" class="mb-4">
|
|
<div class="flex items-center justify-between px-3 mb-2">
|
|
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Recently Used</h3>
|
|
<button
|
|
@click="clearRecentlyUsed"
|
|
class="text-xs text-gray-400 hover:text-gray-600"
|
|
>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-1 px-2">
|
|
<div
|
|
v-for="component in recentlyUsed.slice(0, 4)"
|
|
:key="'recent-' + component.type"
|
|
class="component-item recent-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200 relative"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, component)"
|
|
@dragend="onDragEnd($event)"
|
|
@click="addComponent(component)"
|
|
>
|
|
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
|
<span class="text-xs text-gray-600 text-center">{{ component.name }}</span>
|
|
<div class="absolute top-0 right-0 w-2 h-2 bg-blue-500 rounded-full transform translate-x-1 -translate-y-1"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No Results State -->
|
|
<div v-if="isSearching && filteredComponentsCount === 0" class="p-6 text-center text-gray-500">
|
|
<Icon name="material-symbols:search-off" class="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|
<h3 class="text-sm font-medium text-gray-600 mb-1">No components found</h3>
|
|
<p class="text-xs text-gray-400 mb-3">Try adjusting your search or filter criteria</p>
|
|
<button
|
|
@click="clearSearch"
|
|
class="text-xs text-blue-600 hover:text-blue-700 font-medium"
|
|
>
|
|
Clear search
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Text Inputs Category -->
|
|
<div class="component-category mb-4" v-if="shouldShowCategory('Basic Inputs')">
|
|
<div
|
|
@click="toggleCategory('Basic Inputs')"
|
|
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
|
>
|
|
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Text Inputs</h3>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-xs text-gray-400">
|
|
({{ getVisibleComponentsInCategory('Basic Inputs').length }})
|
|
</span>
|
|
<Icon
|
|
:name="collapsedCategories['Basic Inputs'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
|
class="w-4 h-4 text-gray-400 transition-transform"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!collapsedCategories['Basic Inputs']"
|
|
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
|
>
|
|
<div
|
|
v-for="component in getComponentsByCategory('Basic Inputs')"
|
|
:key="component.type"
|
|
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, component)"
|
|
@dragend="onDragEnd($event)"
|
|
@click="addComponent(component)"
|
|
@mouseenter="showTooltip($event, component)"
|
|
@mouseleave="hideTooltip"
|
|
>
|
|
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
|
<span
|
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
|
v-html="getHighlightedName(component)"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selection & Toggle Category -->
|
|
<div class="component-category mb-4" v-if="shouldShowCategory('Selection Inputs')">
|
|
<div
|
|
@click="toggleCategory('Selection Inputs')"
|
|
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
|
>
|
|
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Selection & Toggle</h3>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-xs text-gray-400">
|
|
({{ getVisibleComponentsInCategory('Selection Inputs').length }})
|
|
</span>
|
|
<Icon
|
|
:name="collapsedCategories['Selection Inputs'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
|
class="w-4 h-4 text-gray-400 transition-transform"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!collapsedCategories['Selection Inputs']"
|
|
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
|
>
|
|
<div
|
|
v-for="component in getComponentsByCategory('Selection Inputs')"
|
|
:key="component.type"
|
|
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, component)"
|
|
@dragend="onDragEnd($event)"
|
|
@click="addComponent(component)"
|
|
@mouseenter="showTooltip($event, component)"
|
|
@mouseleave="hideTooltip"
|
|
>
|
|
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
|
<span
|
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
|
v-html="getHighlightedName(component)"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Date & Pickers Category -->
|
|
<div class="component-category mb-4" v-if="shouldShowCategory('Date and Time')">
|
|
<div
|
|
@click="toggleCategory('Date and Time')"
|
|
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
|
>
|
|
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Date & Pickers</h3>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-xs text-gray-400">
|
|
({{ getVisibleComponentsInCategory('Date and Time').length }})
|
|
</span>
|
|
<Icon
|
|
:name="collapsedCategories['Date and Time'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
|
class="w-4 h-4 text-gray-400 transition-transform"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!collapsedCategories['Date and Time']"
|
|
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
|
>
|
|
<div
|
|
v-for="component in getComponentsByCategory('Date and Time')"
|
|
:key="component.type"
|
|
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, component)"
|
|
@dragend="onDragEnd($event)"
|
|
@click="addComponent(component)"
|
|
@mouseenter="showTooltip($event, component)"
|
|
@mouseleave="hideTooltip"
|
|
>
|
|
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
|
<span
|
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
|
v-html="getHighlightedName(component)"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced & Upload Category -->
|
|
<div class="component-category mb-4" v-if="shouldShowCategory('Advanced')">
|
|
<div
|
|
@click="toggleCategory('Advanced')"
|
|
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
|
>
|
|
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Advanced & Upload</h3>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-xs text-gray-400">
|
|
({{ getVisibleComponentsInCategory('Advanced').length }})
|
|
</span>
|
|
<Icon
|
|
:name="collapsedCategories['Advanced'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
|
class="w-4 h-4 text-gray-400 transition-transform"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!collapsedCategories['Advanced']"
|
|
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
|
>
|
|
<div
|
|
v-for="component in getComponentsByCategory('Advanced')"
|
|
:key="component.type"
|
|
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, component)"
|
|
@dragend="onDragEnd($event)"
|
|
@click="addComponent(component)"
|
|
@mouseenter="showTooltip($event, component)"
|
|
@mouseleave="hideTooltip"
|
|
>
|
|
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
|
<span
|
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
|
v-html="getHighlightedName(component)"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layout & Content Category -->
|
|
<div class="component-category mb-4" v-if="shouldShowCategory('Layout')">
|
|
<div
|
|
@click="toggleCategory('Layout')"
|
|
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
|
>
|
|
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Layout & Content</h3>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-xs text-gray-400">
|
|
({{ getVisibleComponentsInCategory('Layout').length }})
|
|
</span>
|
|
<Icon
|
|
:name="collapsedCategories['Layout'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
|
class="w-4 h-4 text-gray-400 transition-transform"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!collapsedCategories['Layout']"
|
|
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
|
>
|
|
<div
|
|
v-for="component in getComponentsByCategory('Layout')"
|
|
:key="component.type"
|
|
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, component)"
|
|
@dragend="onDragEnd($event)"
|
|
@click="addComponent(component)"
|
|
@mouseenter="showTooltip($event, component)"
|
|
@mouseleave="hideTooltip"
|
|
>
|
|
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
|
<span
|
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
|
v-html="getHighlightedName(component)"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom HTML Category -->
|
|
<div class="component-category mb-4" v-if="shouldShowCategory('Custom HTML')">
|
|
<div
|
|
@click="toggleCategory('Custom HTML')"
|
|
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
|
>
|
|
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Custom HTML</h3>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-xs text-gray-400">
|
|
({{ getVisibleComponentsInCategory('Custom HTML').length }})
|
|
</span>
|
|
<Icon
|
|
:name="collapsedCategories['Custom HTML'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
|
class="w-4 h-4 text-gray-400 transition-transform"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!collapsedCategories['Custom HTML']"
|
|
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
|
>
|
|
<div
|
|
v-for="component in getComponentsByCategory('Custom HTML')"
|
|
:key="component.type"
|
|
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, component)"
|
|
@dragend="onDragEnd($event)"
|
|
@click="addComponent(component)"
|
|
@mouseenter="showTooltip($event, component)"
|
|
@mouseleave="hideTooltip"
|
|
>
|
|
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
|
<span
|
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
|
v-html="getHighlightedName(component)"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tooltip -->
|
|
<div
|
|
v-if="showTooltipData.show"
|
|
ref="tooltip"
|
|
class="tooltip-popup"
|
|
:style="tooltipStyle"
|
|
>
|
|
<div class="tooltip-content">
|
|
<div class="flex items-center space-x-2 mb-2">
|
|
<Icon :name="showTooltipData.component?.icon" class="w-4 h-4 text-gray-600" />
|
|
<span class="font-medium text-sm text-gray-800">{{ showTooltipData.component?.name }}</span>
|
|
</div>
|
|
<p class="text-xs text-gray-600">{{ showTooltipData.component?.description }}</p>
|
|
<div class="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
|
|
<span class="text-xs text-gray-400">{{ showTooltipData.component?.category }}</span>
|
|
<span class="text-xs text-blue-600">Click to add</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const emit = defineEmits(['add-component']);
|
|
|
|
// Search and filtering state
|
|
const searchQuery = ref('');
|
|
const selectedCategories = ref([]);
|
|
const showFilterDropdown = ref(false);
|
|
const showSearchOptions = ref(false);
|
|
const recentlyUsed = ref([]);
|
|
|
|
// Close dropdown when clicking outside
|
|
const closeDropdown = (event) => {
|
|
if (!event.target.closest('.search-container')) {
|
|
showFilterDropdown.value = false;
|
|
showSearchOptions.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', closeDropdown);
|
|
loadRecentlyUsed();
|
|
loadCategoryStates();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', closeDropdown);
|
|
});
|
|
|
|
// Define available form components
|
|
const availableComponents = [
|
|
// Basic inputs
|
|
{
|
|
type: 'text',
|
|
name: 'Text Field',
|
|
category: 'Basic Inputs',
|
|
icon: 'heroicons:document-text',
|
|
description: 'Single line text input',
|
|
defaultProps: {
|
|
type: 'text',
|
|
placeholder: 'Enter text...',
|
|
help: '',
|
|
validation: '',
|
|
readonly: false,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [], // Array of condition objects
|
|
action: 'show', // 'show', 'hide', 'enable', 'disable'
|
|
operator: 'and' // 'and', 'or'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'textarea',
|
|
name: 'Text Area',
|
|
category: 'Basic Inputs',
|
|
icon: 'material-symbols:article-outline',
|
|
description: 'Multi-line text input',
|
|
defaultProps: {
|
|
type: 'textarea',
|
|
placeholder: 'Enter text...',
|
|
help: '',
|
|
validation: '',
|
|
readonly: false,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'Number',
|
|
category: 'Basic Inputs',
|
|
icon: 'material-symbols:counter-1-outline',
|
|
description: 'Numeric input field',
|
|
defaultProps: {
|
|
type: 'number',
|
|
placeholder: '0',
|
|
help: '',
|
|
validation: '',
|
|
readonly: false,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'email',
|
|
name: 'Email',
|
|
category: 'Basic Inputs',
|
|
icon: 'material-symbols:mail-outline',
|
|
description: 'Email address input',
|
|
defaultProps: {
|
|
type: 'email',
|
|
placeholder: 'email@example.com',
|
|
help: '',
|
|
validation: 'email',
|
|
readonly: false,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'password',
|
|
name: 'Password',
|
|
category: 'Basic Inputs',
|
|
icon: 'material-symbols:password',
|
|
description: 'Password input field',
|
|
defaultProps: {
|
|
type: 'password',
|
|
placeholder: 'Enter password...',
|
|
help: '',
|
|
validation: '',
|
|
readonly: false,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'url',
|
|
name: 'URL Field',
|
|
category: 'Basic Inputs',
|
|
icon: 'material-symbols:link',
|
|
description: 'URL/website address input',
|
|
defaultProps: {
|
|
type: 'url',
|
|
placeholder: 'https://example.com',
|
|
help: '',
|
|
validation: 'url',
|
|
readonly: false
|
|
}
|
|
},
|
|
{
|
|
type: 'tel',
|
|
name: 'Phone Field',
|
|
category: 'Basic Inputs',
|
|
icon: 'heroicons:device-phone-mobile',
|
|
description: 'Telephone number input',
|
|
defaultProps: {
|
|
type: 'tel',
|
|
placeholder: '+1 (555) 123-4567',
|
|
help: '',
|
|
validation: '',
|
|
readonly: false
|
|
}
|
|
},
|
|
{
|
|
type: 'mask',
|
|
name: 'Masked Input',
|
|
category: 'Basic Inputs',
|
|
icon: 'heroicons:pencil-square',
|
|
description: 'Input field with custom masking',
|
|
defaultProps: {
|
|
type: 'mask',
|
|
placeholder: 'Enter value...',
|
|
help: 'Input will be formatted according to the mask',
|
|
mask: '###-###-####',
|
|
validation: '',
|
|
readonly: false
|
|
}
|
|
},
|
|
{
|
|
type: 'hidden',
|
|
name: 'Hidden Field',
|
|
category: 'Basic Inputs',
|
|
icon: 'heroicons:eye-slash',
|
|
description: 'Hidden field for storing values',
|
|
defaultProps: {
|
|
type: 'hidden',
|
|
value: '',
|
|
help: 'This field is hidden from users'
|
|
}
|
|
},
|
|
|
|
// Selection inputs
|
|
{
|
|
type: 'select',
|
|
name: 'Select Dropdown',
|
|
category: 'Selection Inputs',
|
|
icon: 'material-symbols:arrow-drop-down-circle-outline',
|
|
description: 'Dropdown select menu',
|
|
defaultProps: {
|
|
type: 'select',
|
|
placeholder: 'Select an option',
|
|
help: '',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' },
|
|
{ label: 'Option 3', value: 'option_3' }
|
|
],
|
|
validation: '',
|
|
readonly: false,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'searchSelect',
|
|
name: 'Searchable Select',
|
|
category: 'Selection Inputs',
|
|
icon: 'material-symbols:search',
|
|
description: 'Dropdown with search functionality',
|
|
defaultProps: {
|
|
type: 'searchSelect',
|
|
placeholder: 'Search and select an option',
|
|
help: 'Type to search for options',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' },
|
|
{ label: 'Option 3', value: 'option_3' }
|
|
],
|
|
validation: '',
|
|
readonly: false,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'checkbox',
|
|
name: 'Checkbox Group',
|
|
category: 'Selection Inputs',
|
|
icon: 'material-symbols:check-box-outline',
|
|
description: 'Multiple checkbox options',
|
|
defaultProps: {
|
|
type: 'checkbox',
|
|
label: 'Checkbox Group',
|
|
name: 'checkbox',
|
|
help: 'Select all that apply',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' },
|
|
{ label: 'Option 3', value: 'option_3' }
|
|
],
|
|
validation: '',
|
|
readonly: false
|
|
}
|
|
},
|
|
{
|
|
type: 'radio',
|
|
name: 'Radio Group',
|
|
category: 'Selection Inputs',
|
|
icon: 'material-symbols:radio-button-checked-outline',
|
|
description: 'Radio button group',
|
|
defaultProps: {
|
|
type: 'radio',
|
|
help: '',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' },
|
|
{ label: 'Option 3', value: 'option_3' }
|
|
],
|
|
validation: '',
|
|
readonly: false
|
|
}
|
|
},
|
|
{
|
|
type: 'switch',
|
|
name: 'Switch Toggle',
|
|
category: 'Selection Inputs',
|
|
icon: 'material-symbols:toggle-on',
|
|
description: 'Modern toggle switch',
|
|
defaultProps: {
|
|
type: 'switch',
|
|
label: 'Enable Option',
|
|
name: 'switch_field',
|
|
help: 'Toggle this option on or off',
|
|
value: false,
|
|
validation: '',
|
|
readonly: false
|
|
}
|
|
},
|
|
|
|
// Date and Time
|
|
{
|
|
type: 'date',
|
|
name: 'Date Picker',
|
|
category: 'Date and Time',
|
|
icon: 'material-symbols:calendar-month-outline',
|
|
description: 'Date selection input',
|
|
defaultProps: {
|
|
type: 'date',
|
|
help: '',
|
|
validation: ''
|
|
}
|
|
},
|
|
{
|
|
type: 'time',
|
|
name: 'Time Picker',
|
|
category: 'Date and Time',
|
|
icon: 'material-symbols:schedule-outline',
|
|
description: 'Time selection input',
|
|
defaultProps: {
|
|
type: 'time',
|
|
help: '',
|
|
validation: ''
|
|
}
|
|
},
|
|
{
|
|
type: 'datetime-local',
|
|
name: 'Date & Time',
|
|
category: 'Date and Time',
|
|
icon: 'material-symbols:event-outline',
|
|
description: 'Date and time selection',
|
|
defaultProps: {
|
|
type: 'datetime-local',
|
|
help: '',
|
|
validation: ''
|
|
}
|
|
},
|
|
{
|
|
type: 'range',
|
|
name: 'Range Slider',
|
|
category: 'Date and Time',
|
|
icon: 'material-symbols:linear-scale',
|
|
description: 'Numeric range slider',
|
|
defaultProps: {
|
|
type: 'range',
|
|
min: 0,
|
|
max: 100,
|
|
step: 1,
|
|
value: 50,
|
|
help: 'Drag to select a value',
|
|
validation: ''
|
|
}
|
|
},
|
|
{
|
|
type: 'color',
|
|
name: 'Color Picker',
|
|
category: 'Date and Time',
|
|
icon: 'material-symbols:palette',
|
|
description: 'Color selection input',
|
|
defaultProps: {
|
|
type: 'color',
|
|
value: '#3b82f6',
|
|
help: 'Click to select a color',
|
|
validation: ''
|
|
}
|
|
},
|
|
|
|
// Advanced
|
|
{
|
|
type: 'file',
|
|
name: 'File Upload',
|
|
category: 'Advanced',
|
|
icon: 'material-symbols:upload-file-outline',
|
|
description: 'File upload input',
|
|
defaultProps: {
|
|
type: 'file',
|
|
help: '',
|
|
accept: '*/*',
|
|
validation: ''
|
|
}
|
|
},
|
|
{
|
|
type: 'image-preview',
|
|
name: 'Image Preview',
|
|
category: 'Advanced',
|
|
icon: 'material-symbols:image-outline',
|
|
description: 'Display an image with preview capabilities',
|
|
defaultProps: {
|
|
label: 'Image Preview',
|
|
name: 'image_preview',
|
|
help: 'Image preview with zoom capability',
|
|
imageUrl: 'https://placehold.co/600x400',
|
|
altText: 'Preview image',
|
|
caption: 'Image caption',
|
|
showZoom: true,
|
|
showCaption: true,
|
|
maxWidth: '100%',
|
|
height: 'auto'
|
|
}
|
|
},
|
|
{
|
|
type: 'repeating-group',
|
|
name: 'Repeating Group',
|
|
category: 'Advanced',
|
|
icon: 'material-symbols:repeat',
|
|
description: 'Collect multiple entries of the same data structure',
|
|
defaultProps: {
|
|
label: 'Person Information',
|
|
name: 'person_group',
|
|
help: 'Add multiple people with their details',
|
|
minItems: 1,
|
|
maxItems: 10,
|
|
buttonText: 'Add Person',
|
|
removeText: 'Remove',
|
|
width: '100%',
|
|
gridColumn: 'span 12',
|
|
showPlaceholder: true, // Whether to show the placeholder in builder mode
|
|
children: [], // Array to hold nested components (draggable fields)
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'dynamic-list',
|
|
name: 'Dynamic List',
|
|
category: 'Advanced',
|
|
icon: 'material-symbols:format-list-bulleted-add',
|
|
description: 'Add multiple items to a dynamic list',
|
|
defaultProps: {
|
|
label: 'Items List',
|
|
name: 'items_list',
|
|
help: 'Add or remove items from the list',
|
|
placeholder: 'Enter item',
|
|
buttonText: 'Add Item',
|
|
minItems: 0,
|
|
maxItems: 20,
|
|
defaultItems: ['Item 1', 'Item 2'],
|
|
itemValidation: '',
|
|
allowDuplicates: true,
|
|
enableSorting: false,
|
|
enableSearch: false,
|
|
itemType: 'text',
|
|
showItemCounter: true,
|
|
confirmDelete: false,
|
|
bulkOperations: false,
|
|
exportFormat: 'json',
|
|
importEnabled: false
|
|
}
|
|
},
|
|
{
|
|
type: 'repeating-table',
|
|
name: 'Data Table',
|
|
category: 'Advanced',
|
|
icon: 'material-symbols:table',
|
|
description: 'Structured table for collecting multiple records',
|
|
defaultProps: {
|
|
label: 'Data Records',
|
|
name: 'data_table',
|
|
help: 'Add records using the form and view them in a table',
|
|
buttonText: 'Add Record',
|
|
editText: 'Edit',
|
|
deleteText: 'Delete',
|
|
minRecords: 0,
|
|
maxRecords: 50,
|
|
showRowNumbers: true,
|
|
allowEdit: true,
|
|
allowDelete: true,
|
|
confirmDelete: true,
|
|
enableSearch: false,
|
|
columns: [
|
|
{
|
|
name: 'name',
|
|
label: 'Name',
|
|
type: 'text',
|
|
required: true,
|
|
placeholder: 'Enter name',
|
|
validation: 'required'
|
|
},
|
|
{
|
|
name: 'email',
|
|
label: 'Email',
|
|
type: 'email',
|
|
required: true,
|
|
placeholder: 'Enter email',
|
|
validation: 'required|email'
|
|
},
|
|
{
|
|
name: 'phone',
|
|
label: 'Phone',
|
|
type: 'tel',
|
|
required: false,
|
|
placeholder: 'Enter phone',
|
|
validation: ''
|
|
}
|
|
],
|
|
defaultData: []
|
|
}
|
|
},
|
|
{
|
|
type: 'otp',
|
|
name: 'OTP Input',
|
|
category: 'Advanced',
|
|
icon: 'heroicons:key',
|
|
description: 'One-time password verification field',
|
|
defaultProps: {
|
|
label: 'Verification Code',
|
|
name: 'otp_code',
|
|
help: 'Enter the verification code sent to you',
|
|
digits: 6,
|
|
validation: 'required'
|
|
}
|
|
},
|
|
{
|
|
type: 'dropzone',
|
|
name: 'File Dropzone',
|
|
category: 'Advanced',
|
|
icon: 'heroicons:cloud-arrow-up',
|
|
description: 'Drag & drop file upload area',
|
|
defaultProps: {
|
|
label: 'Upload Files',
|
|
name: 'file_upload',
|
|
help: 'Drag files here or click to browse',
|
|
accept: 'image/*,.pdf,.doc,.docx',
|
|
multiple: true,
|
|
maxSize: 5242880, // 5MB
|
|
maxFiles: 5,
|
|
validation: ''
|
|
}
|
|
},
|
|
{
|
|
type: 'button',
|
|
name: 'Button',
|
|
category: 'Advanced',
|
|
icon: 'material-symbols:smart-button',
|
|
description: 'Action button for forms',
|
|
defaultProps: {
|
|
label: 'Click Me',
|
|
name: 'action_button',
|
|
help: 'Button for triggering actions',
|
|
buttonType: 'button', // button, submit, reset
|
|
variant: 'primary', // primary, secondary, success, warning, danger
|
|
size: 'md', // sm, md, lg
|
|
disabled: false,
|
|
showLabel: true, // Whether to show the label above the button
|
|
showButtonText: true, // Whether to show text on the button
|
|
buttonText: '', // Text to display on button (falls back to label if empty)
|
|
icon: '', // Optional icon to display on button
|
|
onClick: '' // Custom JavaScript code to execute
|
|
}
|
|
},
|
|
|
|
// Layout
|
|
{
|
|
type: 'form-section',
|
|
name: 'Form Section',
|
|
category: 'Layout',
|
|
icon: 'material-symbols:view-module-outline',
|
|
description: 'Container to group related form fields into sections',
|
|
defaultProps: {
|
|
label: 'Section Title',
|
|
description: 'Optional section description',
|
|
collapsible: false,
|
|
collapsed: false,
|
|
showBorder: true,
|
|
borderStyle: 'solid', // solid, dashed, dotted
|
|
backgroundColor: '#ffffff',
|
|
headerBackground: '#f9fafb',
|
|
showHeader: true,
|
|
headerSize: 'medium', // small, medium, large
|
|
spacing: 'normal', // compact, normal, relaxed
|
|
width: '100%',
|
|
gridColumn: 'span 12',
|
|
showPlaceholder: true, // Whether to show the placeholder in builder mode
|
|
children: [], // Array to hold nested components
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'info-display',
|
|
name: 'Info Display',
|
|
category: 'Layout',
|
|
icon: 'material-symbols:info-outline',
|
|
description: 'Display read-only information in key-value format with optional buttons',
|
|
defaultProps: {
|
|
name: 'info_display',
|
|
title: 'Information',
|
|
help: 'Display important information in an organized format',
|
|
fields: [
|
|
{ label: 'Customer Name', value: 'John Doe', key: 'customer_name', type: 'text' },
|
|
{ label: 'Email', value: 'john@example.com', key: 'customer_email', type: 'text' },
|
|
{ label: 'Phone', value: '+1-234-567-8900', key: 'customer_phone', type: 'text' },
|
|
{ label: 'View Profile', value: 'View Details', key: 'profile_link', type: 'button', url: 'https://example.com/profile', openInNewTab: true, icon: 'material-symbols:link' }
|
|
],
|
|
layout: 'side-by-side', // vertical, horizontal, grid, side-by-side
|
|
showBorder: true,
|
|
backgroundColor: '#f9fafb',
|
|
width: '100%',
|
|
gridColumn: 'span 12'
|
|
}
|
|
},
|
|
{
|
|
type: 'heading',
|
|
name: 'Heading',
|
|
category: 'Layout',
|
|
icon: 'material-symbols:title',
|
|
description: 'Section heading text',
|
|
defaultProps: {
|
|
value: 'Section Heading',
|
|
level: 2,
|
|
width: '100%',
|
|
gridColumn: 'span 12'
|
|
}
|
|
},
|
|
{
|
|
type: 'paragraph',
|
|
name: 'Paragraph',
|
|
category: 'Layout',
|
|
icon: 'material-symbols:text-snippet-outline',
|
|
description: 'Paragraph of text',
|
|
defaultProps: {
|
|
value: 'Enter some descriptive text here.',
|
|
width: '100%',
|
|
gridColumn: 'span 12'
|
|
}
|
|
},
|
|
{
|
|
type: 'divider',
|
|
name: 'Divider',
|
|
category: 'Layout',
|
|
icon: 'material-symbols:horizontal-rule',
|
|
description: 'Horizontal divider line',
|
|
defaultProps: {}
|
|
},
|
|
{
|
|
type: 'layout-grid',
|
|
name: 'Layout Grid',
|
|
category: 'Layout',
|
|
icon: 'material-symbols:grid-on',
|
|
description: 'Custom grid layout with drag-and-drop components and spanning capabilities',
|
|
defaultProps: {
|
|
label: '',
|
|
help: '',
|
|
rows: 2,
|
|
columns: 2,
|
|
gap: 8,
|
|
cells: [], // Array of cell objects with components and span settings
|
|
width: '100%',
|
|
gridColumn: 'span 12',
|
|
showPlaceholder: true,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'customHtml',
|
|
name: 'Custom HTML',
|
|
category: 'Custom HTML',
|
|
icon: 'material-symbols:code',
|
|
description: 'Custom HTML with CSS and JavaScript',
|
|
defaultProps: {
|
|
label: 'Custom HTML Component',
|
|
name: 'custom_html',
|
|
help: 'Create custom designs using HTML, CSS, and JavaScript',
|
|
htmlContent: `<div class="custom-component">
|
|
<h3>Custom HTML Component</h3>
|
|
<p>Edit this HTML to create your custom design.</p>
|
|
<button type="button">Click me!</button>
|
|
</div>`,
|
|
cssContent: `.custom-component {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
text-align: center;
|
|
}
|
|
|
|
.custom-component h3 {
|
|
color: #1e40af;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.custom-component button {
|
|
background: #3b82f6;
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.custom-component button:hover {
|
|
background: #2563eb;
|
|
}`,
|
|
jsContent: `// Custom JavaScript for this component
|
|
// You can access the component element via 'this.element'
|
|
// Available functions:
|
|
// - this.element: The HTML element of this component
|
|
// - this.getValue(): Get form values
|
|
// - this.setValue(name, value): Set form field values
|
|
|
|
console.log('Custom HTML component initialized');
|
|
|
|
// Example: Add click handler
|
|
const buttons = this.element.querySelectorAll('button');
|
|
buttons.forEach(button => {
|
|
button.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
console.log('Custom button clicked!');
|
|
});
|
|
});`,
|
|
allowScripts: true,
|
|
previewMode: 'safe', // 'safe' or 'advanced'
|
|
width: '100%',
|
|
gridColumn: 'span 12',
|
|
showInPreview: true,
|
|
// Conditional Logic Properties
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'customHtml',
|
|
name: 'Price Calculator',
|
|
category: 'Custom HTML',
|
|
icon: 'material-symbols:calculate',
|
|
description: 'Interactive price calculator with real-time updates',
|
|
defaultProps: {
|
|
label: 'Price Calculator',
|
|
name: 'price_calculator',
|
|
help: 'Calculate total price based on quantity and unit price',
|
|
htmlContent: `<div class="price-calculator">
|
|
<h3>Price Calculator</h3>
|
|
<div class="calculator-grid">
|
|
<div class="input-group">
|
|
<label>Quantity:</label>
|
|
<input type="number" id="quantity" value="1" min="1" class="calc-input">
|
|
</div>
|
|
<div class="input-group">
|
|
<label>Unit Price:</label>
|
|
<input type="number" id="unit-price" value="10.00" step="0.01" class="calc-input">
|
|
</div>
|
|
<div class="result-group">
|
|
<label>Total Price:</label>
|
|
<div id="total-price" class="total-display">$10.00</div>
|
|
</div>
|
|
</div>
|
|
<div class="discount-section">
|
|
<label>
|
|
<input type="checkbox" id="discount-checkbox"> Apply 10% discount
|
|
</label>
|
|
<div id="discount-amount" class="discount-display" style="display: none;">-$1.00</div>
|
|
</div>
|
|
</div>`,
|
|
cssContent: `.price-calculator {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.price-calculator h3 {
|
|
color: #059669;
|
|
margin-bottom: 15px;
|
|
text-align: center;
|
|
}
|
|
|
|
.calculator-grid {
|
|
display: grid;
|
|
gap: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.input-group, .result-group {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.calc-input {
|
|
width: 120px;
|
|
padding: 6px 8px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.total-display {
|
|
font-weight: bold;
|
|
font-size: 18px;
|
|
color: #059669;
|
|
background: #f0fdf4;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
border: 1px solid #10b981;
|
|
}
|
|
|
|
.discount-section {
|
|
border-top: 1px solid #d1d5db;
|
|
padding-top: 15px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.discount-display {
|
|
font-weight: bold;
|
|
color: #dc2626;
|
|
background: #fef2f2;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
border: 1px solid #fca5a5;
|
|
}`,
|
|
jsContent: `// Price Calculator JavaScript
|
|
console.log('Price calculator initialized');
|
|
|
|
// Get form field values
|
|
const quantity = this.getValue('quantity') || 1;
|
|
const unitPrice = this.getValue('unit_price') || 10.00;
|
|
|
|
// Initialize calculator
|
|
function updateCalculator() {
|
|
const quantityInput = this.element.querySelector('#quantity');
|
|
const unitPriceInput = this.element.querySelector('#unit-price');
|
|
const totalDisplay = this.element.querySelector('#total-price');
|
|
const discountCheckbox = this.element.querySelector('#discount-checkbox');
|
|
const discountDisplay = this.element.querySelector('#discount-amount');
|
|
|
|
// Set initial values from form data
|
|
if (quantityInput) quantityInput.value = quantity;
|
|
if (unitPriceInput) unitPriceInput.value = unitPrice;
|
|
|
|
function calculateTotal() {
|
|
const qty = parseFloat(quantityInput.value) || 0;
|
|
const price = parseFloat(unitPriceInput.value) || 0;
|
|
const subtotal = qty * price;
|
|
const discount = discountCheckbox.checked ? subtotal * 0.1 : 0;
|
|
const total = subtotal - discount;
|
|
|
|
// Update displays
|
|
totalDisplay.textContent = '$' + total.toFixed(2);
|
|
discountDisplay.textContent = '-$' + discount.toFixed(2);
|
|
discountDisplay.style.display = discountCheckbox.checked ? 'block' : 'none';
|
|
|
|
// Update form fields
|
|
this.setValue('quantity', qty);
|
|
this.setValue('unit_price', price);
|
|
this.setValue('total_price', total);
|
|
this.setValue('discount_applied', discountCheckbox.checked);
|
|
}
|
|
|
|
// Add event listeners
|
|
quantityInput.addEventListener('input', calculateTotal.bind(this));
|
|
unitPriceInput.addEventListener('input', calculateTotal.bind(this));
|
|
discountCheckbox.addEventListener('change', calculateTotal.bind(this));
|
|
|
|
// Initial calculation
|
|
calculateTotal.call(this);
|
|
}
|
|
|
|
// Initialize when component loads
|
|
updateCalculator.call(this);`,
|
|
allowScripts: true,
|
|
previewMode: 'advanced',
|
|
width: '100%',
|
|
gridColumn: 'span 12',
|
|
showInPreview: true,
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'customHtml',
|
|
name: 'Progress Tracker',
|
|
category: 'Custom HTML',
|
|
icon: 'material-symbols:trending-up',
|
|
description: 'Visual progress tracking with dynamic updates',
|
|
defaultProps: {
|
|
label: 'Progress Tracker',
|
|
name: 'progress_tracker',
|
|
help: 'Track completion progress with visual indicators',
|
|
htmlContent: `<div class="progress-tracker">
|
|
<h3>Application Progress</h3>
|
|
<div class="progress-steps">
|
|
<div class="step" data-step="1">
|
|
<div class="step-number">1</div>
|
|
<div class="step-label">Personal Info</div>
|
|
<div class="step-status">Completed</div>
|
|
</div>
|
|
<div class="step" data-step="2">
|
|
<div class="step-number">2</div>
|
|
<div class="step-label">Documents</div>
|
|
<div class="step-status">In Progress</div>
|
|
</div>
|
|
<div class="step" data-step="3">
|
|
<div class="step-number">3</div>
|
|
<div class="step-label">Review</div>
|
|
<div class="step-status">Pending</div>
|
|
</div>
|
|
<div class="step" data-step="4">
|
|
<div class="step-number">4</div>
|
|
<div class="step-label">Submit</div>
|
|
<div class="step-status">Pending</div>
|
|
</div>
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progress-fill"></div>
|
|
</div>
|
|
<div class="progress-text">
|
|
<span id="progress-percentage">25%</span> Complete
|
|
</div>
|
|
</div>`,
|
|
cssContent: `.progress-tracker {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.progress-tracker h3 {
|
|
color: #7c3aed;
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.progress-steps {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 20px;
|
|
position: relative;
|
|
}
|
|
|
|
.progress-steps::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 15px;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: #e5e7eb;
|
|
z-index: 1;
|
|
}
|
|
|
|
.step {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.step-number {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
background: #e5e7eb;
|
|
color: #6b7280;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
margin-bottom: 8px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.step.completed .step-number {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.step.current .step-number {
|
|
background: #3b82f6;
|
|
color: white;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.step-label {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
text-align: center;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.step-status {
|
|
font-size: 10px;
|
|
color: #6b7280;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: #e5e7eb;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #10b981, #3b82f6);
|
|
width: 25%;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.progress-text {
|
|
text-align: center;
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { transform: scale(1); }
|
|
50% { transform: scale(1.1); }
|
|
}`,
|
|
jsContent: `// Progress Tracker JavaScript
|
|
console.log('Progress tracker initialized');
|
|
|
|
// Get current step from form data
|
|
const currentStep = this.getValue('current_step') || 1;
|
|
|
|
function updateProgress() {
|
|
const steps = this.element.querySelectorAll('.step');
|
|
const progressFill = this.element.querySelector('#progress-fill');
|
|
const progressPercentage = this.element.querySelector('#progress-percentage');
|
|
|
|
// Update step states
|
|
steps.forEach((step, index) => {
|
|
const stepNum = index + 1;
|
|
step.classList.remove('completed', 'current');
|
|
|
|
if (stepNum < currentStep) {
|
|
step.classList.add('completed');
|
|
} else if (stepNum === currentStep) {
|
|
step.classList.add('current');
|
|
}
|
|
});
|
|
|
|
// Update progress bar
|
|
const percentage = (currentStep / steps.length) * 100;
|
|
progressFill.style.width = percentage + '%';
|
|
progressPercentage.textContent = Math.round(percentage) + '%';
|
|
|
|
// Update form data
|
|
this.setValue('progress_percentage', percentage);
|
|
}
|
|
|
|
// Add click handlers to steps
|
|
const steps = this.element.querySelectorAll('.step');
|
|
steps.forEach((step, index) => {
|
|
step.addEventListener('click', () => {
|
|
const newStep = index + 1;
|
|
this.setValue('current_step', newStep);
|
|
updateProgress.call(this);
|
|
});
|
|
});
|
|
|
|
// Initialize progress
|
|
updateProgress.call(this);`,
|
|
allowScripts: true,
|
|
previewMode: 'advanced',
|
|
width: '100%',
|
|
gridColumn: 'span 12',
|
|
showInPreview: true,
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'customHtml',
|
|
name: 'File Upload Preview',
|
|
category: 'Custom HTML',
|
|
icon: 'material-symbols:upload-file',
|
|
description: 'Custom file upload with preview and validation',
|
|
defaultProps: {
|
|
label: 'File Upload Preview',
|
|
name: 'file_upload_preview',
|
|
help: 'Upload files with preview and validation',
|
|
htmlContent: `<div class="file-upload-area">
|
|
<h3>Document Upload</h3>
|
|
<div class="upload-zone" id="upload-zone">
|
|
<div class="upload-icon">📁</div>
|
|
<p>Drag and drop files here or click to browse</p>
|
|
<input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.jpg,.jpeg,.png" style="display: none;">
|
|
</div>
|
|
<div class="file-list" id="file-list">
|
|
<!-- Files will be listed here -->
|
|
</div>
|
|
<div class="upload-status" id="upload-status"></div>
|
|
</div>`,
|
|
cssContent: `.file-upload-area {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.file-upload-area h3 {
|
|
color: #d97706;
|
|
margin-bottom: 15px;
|
|
text-align: center;
|
|
}
|
|
|
|
.upload-zone {
|
|
border: 2px dashed #f59e0b;
|
|
border-radius: 8px;
|
|
padding: 30px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
background: white;
|
|
}
|
|
|
|
.upload-zone:hover {
|
|
border-color: #d97706;
|
|
background: #fffbeb;
|
|
}
|
|
|
|
.upload-zone.dragover {
|
|
border-color: #10b981;
|
|
background: #f0fdf4;
|
|
}
|
|
|
|
.upload-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.file-list {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.file-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 4px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.file-name {
|
|
font-weight: 500;
|
|
color: #374151;
|
|
}
|
|
|
|
.file-size {
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.file-status {
|
|
font-size: 12px;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.file-status.valid {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.file-status.invalid {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.upload-status {
|
|
margin-top: 10px;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
}`,
|
|
jsContent: `// File Upload Preview JavaScript
|
|
console.log('File upload preview initialized');
|
|
|
|
const uploadZone = this.element.querySelector('#upload-zone');
|
|
const fileInput = this.element.querySelector('#file-input');
|
|
const fileList = this.element.querySelector('#file-list');
|
|
const uploadStatus = this.element.querySelector('#upload-status');
|
|
|
|
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/jpeg', 'image/png'];
|
|
const maxFileSize = 5 * 1024 * 1024; // 5MB
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function validateFile(file) {
|
|
const errors = [];
|
|
|
|
if (!allowedTypes.includes(file.type)) {
|
|
errors.push('Invalid file type');
|
|
}
|
|
|
|
if (file.size > maxFileSize) {
|
|
errors.push('File too large (max 5MB)');
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
function createFileItem(file) {
|
|
const fileItem = document.createElement('div');
|
|
fileItem.className = 'file-item';
|
|
|
|
const errors = validateFile(file);
|
|
const isValid = errors.length === 0;
|
|
|
|
fileItem.innerHTML = \`
|
|
<div>
|
|
<div class="file-name">\${file.name}</div>
|
|
<div class="file-size">\${formatFileSize(file.size)}</div>
|
|
</div>
|
|
<div class="file-status \${isValid ? 'valid' : 'invalid'}">
|
|
\${isValid ? '✓ Valid' : '✗ ' + errors.join(', ')}
|
|
</div>
|
|
\`;
|
|
|
|
return fileItem;
|
|
}
|
|
|
|
function handleFiles(files) {
|
|
fileList.innerHTML = '';
|
|
let validCount = 0;
|
|
let totalCount = files.length;
|
|
|
|
Array.from(files).forEach(file => {
|
|
const fileItem = createFileItem(file);
|
|
fileList.appendChild(fileItem);
|
|
|
|
if (validateFile(file).length === 0) {
|
|
validCount++;
|
|
}
|
|
});
|
|
|
|
// Update status
|
|
uploadStatus.textContent = \`\${validCount} of \${totalCount} files valid\`;
|
|
uploadStatus.className = \`upload-status \${validCount === totalCount ? 'valid' : 'invalid'}\`;
|
|
|
|
// Update form data
|
|
this.setValue('uploaded_files', Array.from(files).map(f => f.name));
|
|
this.setValue('valid_files_count', validCount);
|
|
this.setValue('total_files_count', totalCount);
|
|
}
|
|
|
|
// Event listeners
|
|
uploadZone.addEventListener('click', () => fileInput.click());
|
|
|
|
uploadZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
uploadZone.classList.add('dragover');
|
|
});
|
|
|
|
uploadZone.addEventListener('dragleave', () => {
|
|
uploadZone.classList.remove('dragover');
|
|
});
|
|
|
|
uploadZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
uploadZone.classList.remove('dragover');
|
|
handleFiles.call(this, e.dataTransfer.files);
|
|
});
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
handleFiles.call(this, e.target.files);
|
|
});`,
|
|
allowScripts: true,
|
|
previewMode: 'advanced',
|
|
width: '100%',
|
|
gridColumn: 'span 12',
|
|
showInPreview: true,
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
// Get all available categories
|
|
const availableCategories = computed(() => {
|
|
return [...new Set(availableComponents.map(component => component.category))];
|
|
});
|
|
|
|
// Get components by category for rendering in sections
|
|
const getComponentsByCategory = (category) => {
|
|
return availableComponents.filter(component => component.category === category);
|
|
};
|
|
|
|
// Get filtered components count for empty state
|
|
const filteredComponentsCount = computed(() => {
|
|
let components = availableComponents;
|
|
|
|
// Apply category filter
|
|
if (selectedCategories.value.length > 0) {
|
|
components = components.filter(component =>
|
|
selectedCategories.value.includes(component.category)
|
|
);
|
|
}
|
|
|
|
// Apply search filter
|
|
if (searchQuery.value) {
|
|
components = components.filter(component => matchesSearch(component));
|
|
}
|
|
|
|
return components.length;
|
|
});
|
|
|
|
// Check if we are in search mode
|
|
const isSearching = computed(() => {
|
|
return searchQuery.value.length > 0 || selectedCategories.value.length > 0;
|
|
});
|
|
|
|
// Check if there are active filters
|
|
const hasActiveFilters = computed(() => {
|
|
return selectedCategories.value.length > 0;
|
|
});
|
|
|
|
// Enhanced search matching with highlighting support
|
|
const matchesSearch = (component) => {
|
|
// Always show if no search query
|
|
if (!searchQuery.value) {
|
|
// But apply category filter if active
|
|
if (selectedCategories.value.length > 0) {
|
|
return selectedCategories.value.includes(component.category);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const query = searchQuery.value.toLowerCase();
|
|
const matchesText = (
|
|
component.name.toLowerCase().includes(query) ||
|
|
component.description.toLowerCase().includes(query) ||
|
|
component.type.toLowerCase().includes(query) ||
|
|
component.category.toLowerCase().includes(query)
|
|
);
|
|
|
|
// Apply both search and category filter
|
|
if (selectedCategories.value.length > 0) {
|
|
return matchesText && selectedCategories.value.includes(component.category);
|
|
}
|
|
|
|
return matchesText;
|
|
};
|
|
|
|
// Filter functions
|
|
const toggleFilterDropdown = () => {
|
|
showFilterDropdown.value = !showFilterDropdown.value;
|
|
showSearchOptions.value = false;
|
|
};
|
|
|
|
const removeCategory = (category) => {
|
|
selectedCategories.value = selectedCategories.value.filter(c => c !== category);
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
selectedCategories.value = [];
|
|
showFilterDropdown.value = false;
|
|
};
|
|
|
|
const clearSearch = () => {
|
|
searchQuery.value = '';
|
|
selectedCategories.value = [];
|
|
};
|
|
|
|
// Recently used functions
|
|
const addToRecentlyUsed = (component) => {
|
|
// Remove if already exists
|
|
recentlyUsed.value = recentlyUsed.value.filter(c => c.type !== component.type);
|
|
// Add to beginning
|
|
recentlyUsed.value.unshift(component);
|
|
// Keep only last 8
|
|
recentlyUsed.value = recentlyUsed.value.slice(0, 8);
|
|
// Save to localStorage
|
|
saveRecentlyUsed();
|
|
};
|
|
|
|
const loadRecentlyUsed = () => {
|
|
try {
|
|
const saved = localStorage.getItem('formBuilder_recentComponents');
|
|
if (saved) {
|
|
const parsed = JSON.parse(saved);
|
|
// Validate that components still exist
|
|
recentlyUsed.value = parsed.filter(component =>
|
|
availableComponents.some(c => c.type === component.type)
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load recently used components:', error);
|
|
recentlyUsed.value = [];
|
|
}
|
|
};
|
|
|
|
const saveRecentlyUsed = () => {
|
|
try {
|
|
localStorage.setItem('formBuilder_recentComponents', JSON.stringify(recentlyUsed.value));
|
|
} catch (error) {
|
|
console.warn('Failed to save recently used components:', error);
|
|
}
|
|
};
|
|
|
|
const clearRecentlyUsed = () => {
|
|
recentlyUsed.value = [];
|
|
localStorage.removeItem('formBuilder_recentComponents');
|
|
};
|
|
|
|
// Category collapse/expand functionality
|
|
const collapsedCategories = ref({});
|
|
|
|
const toggleCategory = (category) => {
|
|
collapsedCategories.value[category] = !collapsedCategories.value[category];
|
|
// Save state to localStorage
|
|
localStorage.setItem('formBuilder_collapsedCategories', JSON.stringify(collapsedCategories.value));
|
|
};
|
|
|
|
const loadCategoryStates = () => {
|
|
try {
|
|
const saved = localStorage.getItem('formBuilder_collapsedCategories');
|
|
if (saved) {
|
|
collapsedCategories.value = JSON.parse(saved);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load category states:', error);
|
|
}
|
|
};
|
|
|
|
// Category visibility functions
|
|
const shouldShowCategory = (category) => {
|
|
const componentsInCategory = getComponentsByCategory(category);
|
|
const visibleComponents = componentsInCategory.filter(component => matchesSearch(component));
|
|
return visibleComponents.length > 0;
|
|
};
|
|
|
|
const getVisibleComponentsInCategory = (category) => {
|
|
const componentsInCategory = getComponentsByCategory(category);
|
|
return componentsInCategory.filter(component => matchesSearch(component));
|
|
};
|
|
|
|
// Component highlighting
|
|
const highlightComponent = (component) => {
|
|
if (!searchQuery.value) return false;
|
|
const query = searchQuery.value.toLowerCase();
|
|
return component.name.toLowerCase().includes(query);
|
|
};
|
|
|
|
// Tooltip functionality
|
|
const showTooltipData = ref({ show: false, component: null });
|
|
const tooltip = ref(null);
|
|
const tooltipStyle = ref({});
|
|
|
|
const showTooltip = (event, component) => {
|
|
showTooltipData.value = { show: true, component };
|
|
|
|
nextTick(() => {
|
|
const rect = event.target.getBoundingClientRect();
|
|
const tooltipEl = tooltip.value;
|
|
|
|
if (tooltipEl) {
|
|
const tooltipRect = tooltipEl.getBoundingClientRect();
|
|
|
|
// Position tooltip to the right of the component
|
|
let left = rect.right + 8;
|
|
let top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
|
|
|
|
// Adjust if tooltip would go off screen
|
|
if (left + tooltipRect.width > window.innerWidth) {
|
|
left = rect.left - tooltipRect.width - 8;
|
|
}
|
|
|
|
if (top < 0) {
|
|
top = 8;
|
|
} else if (top + tooltipRect.height > window.innerHeight) {
|
|
top = window.innerHeight - tooltipRect.height - 8;
|
|
}
|
|
|
|
tooltipStyle.value = {
|
|
position: 'fixed',
|
|
left: `${left}px`,
|
|
top: `${top}px`,
|
|
zIndex: 1000
|
|
};
|
|
}
|
|
});
|
|
};
|
|
|
|
const hideTooltip = () => {
|
|
showTooltipData.value = { show: false, component: null };
|
|
};
|
|
|
|
// Search highlighting functionality
|
|
const highlightSearchTerm = (text, searchTerm) => {
|
|
if (!searchTerm || !text) return text;
|
|
|
|
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
return text.replace(regex, '<mark class="search-highlight">$1</mark>');
|
|
};
|
|
|
|
const getHighlightedName = (component) => {
|
|
if (!searchQuery.value) return component.name;
|
|
return highlightSearchTerm(component.name, searchQuery.value);
|
|
};
|
|
|
|
// Handle drag start event
|
|
const onDragStart = (event, component) => {
|
|
// Just set the basic component data, the optimal grid placement
|
|
// will be calculated in the store when adding the component
|
|
const componentWithGrid = {
|
|
...component,
|
|
defaultProps: {
|
|
...component.defaultProps,
|
|
width: '100%'
|
|
// Note: gridColumn is now determined by the store's findOptimalGridPlacement method
|
|
}
|
|
};
|
|
|
|
// Prepare the JSON data
|
|
const jsonData = JSON.stringify(componentWithGrid);
|
|
|
|
try {
|
|
// For Mac/Safari compatibility, use multiple data formats
|
|
event.dataTransfer.effectAllowed = 'copy';
|
|
|
|
// Add data in multiple formats for maximum compatibility
|
|
// text/plain is the most universally supported
|
|
event.dataTransfer.setData('text/plain', jsonData);
|
|
|
|
// Custom format - might not work in all browsers but provides context
|
|
try {
|
|
event.dataTransfer.setData('component', jsonData);
|
|
} catch (err) {
|
|
console.log('Custom format not supported in this browser');
|
|
}
|
|
|
|
// Store component data in a global variable as fallback for Safari
|
|
window.__draggedComponentData = componentWithGrid;
|
|
|
|
// Add visual feedback
|
|
event.target.classList.add('dragging');
|
|
|
|
// Set a drag image
|
|
try {
|
|
const dragIcon = document.createElement('div');
|
|
dragIcon.className = 'drag-preview';
|
|
dragIcon.innerHTML = `<div class="p-2 bg-blue-100 border border-blue-300 rounded text-xs">${component.name}</div>`;
|
|
dragIcon.style.position = 'absolute';
|
|
dragIcon.style.top = '-1000px';
|
|
document.body.appendChild(dragIcon);
|
|
|
|
event.dataTransfer.setDragImage(dragIcon, 0, 0);
|
|
|
|
// Clean up the drag image element after a short delay
|
|
setTimeout(() => {
|
|
document.body.removeChild(dragIcon);
|
|
}, 100);
|
|
} catch (err) {
|
|
console.log('Custom drag image not supported in this browser');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in drag start:', error);
|
|
}
|
|
};
|
|
|
|
// Add a component directly via click
|
|
const addComponent = (component) => {
|
|
addToRecentlyUsed(component);
|
|
emit('add-component', component);
|
|
};
|
|
|
|
// Handle drag end event
|
|
const onDragEnd = (event) => {
|
|
event.target.classList.remove('dragging');
|
|
|
|
// Clear the fallback data after a short delay
|
|
setTimeout(() => {
|
|
window.__draggedComponentData = null;
|
|
}, 300);
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-components {
|
|
@apply bg-white h-full relative;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.component-category {
|
|
@apply mb-3;
|
|
}
|
|
|
|
.component-category h3 {
|
|
@apply text-xs font-semibold uppercase tracking-wider text-gray-700;
|
|
}
|
|
|
|
/* Enhanced component items */
|
|
.component-item {
|
|
@apply h-20;
|
|
transition: all 0.15s ease-in-out;
|
|
min-height: 4.5rem;
|
|
position: relative;
|
|
}
|
|
|
|
.component-item.enhanced-card {
|
|
@apply shadow-sm;
|
|
}
|
|
|
|
.component-item.enhanced-card:hover {
|
|
@apply shadow-md border-gray-300;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.component-item.highlight-match {
|
|
@apply ring-2 ring-yellow-200 bg-yellow-50 border-yellow-300;
|
|
}
|
|
|
|
.component-item.recent-item::after {
|
|
content: '';
|
|
@apply absolute top-0 right-0 w-2 h-2 bg-blue-500 rounded-full transform translate-x-1 -translate-y-1;
|
|
}
|
|
|
|
.component-item .iconify {
|
|
@apply w-5 h-5 mb-1;
|
|
}
|
|
|
|
.component-item span {
|
|
@apply leading-tight;
|
|
}
|
|
|
|
.component-item:active {
|
|
transform: scale(0.97);
|
|
}
|
|
|
|
.component-item.dragging {
|
|
opacity: 0.5;
|
|
background-color: #e0f2fe;
|
|
border-color: #60a5fa;
|
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.3);
|
|
}
|
|
|
|
/* Tooltip styles */
|
|
.tooltip-popup {
|
|
@apply bg-white border border-gray-200 rounded-lg shadow-lg;
|
|
max-width: 250px;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.tooltip-content {
|
|
@apply p-3;
|
|
}
|
|
|
|
/* Filter dropdown styles */
|
|
.search-container {
|
|
position: relative;
|
|
}
|
|
|
|
/* Category header hover effects */
|
|
.component-category > div:first-child:hover {
|
|
@apply bg-gray-50;
|
|
}
|
|
|
|
/* Smooth transitions for category expansion */
|
|
.component-category .grid {
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
|
|
/* Search highlight styles */
|
|
.highlight-text {
|
|
@apply bg-yellow-200;
|
|
}
|
|
|
|
.search-highlight,
|
|
:deep(.search-highlight) {
|
|
@apply bg-yellow-200 px-1 py-0.5 rounded text-gray-800 font-medium;
|
|
background-color: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.component-name {
|
|
word-break: break-word;
|
|
}
|
|
|
|
.component-name :deep(mark) {
|
|
@apply bg-yellow-200 px-1 py-0.5 rounded text-gray-800 font-medium;
|
|
background-color: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
/* Enhanced search input */
|
|
.search-container input:focus {
|
|
@apply ring-2 ring-blue-500 border-blue-500;
|
|
}
|
|
|
|
/* Filter chip styles */
|
|
.filter-chip {
|
|
@apply inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full;
|
|
}
|
|
|
|
.filter-chip button {
|
|
@apply ml-1 hover:bg-blue-200 rounded-full p-0.5 transition-colors;
|
|
}
|
|
|
|
/* Recently used indicator */
|
|
.recent-indicator {
|
|
@apply absolute top-0 right-0 w-2 h-2 bg-blue-500 rounded-full transform translate-x-1 -translate-y-1;
|
|
}
|
|
|
|
/* Empty state styles */
|
|
.empty-state {
|
|
@apply text-center text-gray-500 py-8;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
@apply text-sm font-medium text-gray-600 mb-1;
|
|
}
|
|
|
|
.empty-state p {
|
|
@apply text-xs text-gray-400 mb-3;
|
|
}
|
|
|
|
.drag-preview {
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 640px) {
|
|
.component-item {
|
|
@apply h-16;
|
|
min-height: 4rem;
|
|
}
|
|
|
|
.component-item .iconify {
|
|
@apply w-4 h-4;
|
|
}
|
|
|
|
.component-item span {
|
|
@apply text-xs;
|
|
}
|
|
}
|
|
|
|
/* Animation for category expand/collapse */
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.component-category .grid {
|
|
animation: slideDown 0.2s ease-out;
|
|
}
|
|
</style> |