Enhance FormBuilderComponents with Search and Filter Functionality
- 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.
This commit is contained in:
parent
3abc215b42
commit
07539e2344
@ -4,8 +4,9 @@
|
|||||||
"Bash(mkdir:*)",
|
"Bash(mkdir:*)",
|
||||||
"Bash(yarn dev:*)",
|
"Bash(yarn dev:*)",
|
||||||
"Bash(yarn lint:*)",
|
"Bash(yarn lint:*)",
|
||||||
"Bash(yarn build:*)"
|
"Bash(yarn build:*)",
|
||||||
|
"Bash(npx eslint:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,30 +1,112 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-components">
|
<div class="form-components">
|
||||||
<!-- Search Bar -->
|
<!-- Enhanced Search Bar -->
|
||||||
<div class="search-container p-3 mb-2">
|
<div class="search-container p-3 mb-2">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="Search..."
|
placeholder="Search components..."
|
||||||
class="w-full px-3 py-2 pl-9 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"
|
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
|
<Icon
|
||||||
name="material-symbols:search"
|
name="material-symbols:search"
|
||||||
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Text Inputs Category -->
|
<!-- Category Filter Chips -->
|
||||||
<div class="component-category mb-4">
|
<div v-if="selectedCategories.length > 0" class="px-3 mb-3">
|
||||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Text Inputs</h3>
|
<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 class="grid grid-cols-2 gap-1 px-2">
|
||||||
<div
|
<div
|
||||||
v-for="component in getComponentsByCategory('Basic Inputs')"
|
v-for="component in recentlyUsed.slice(0, 4)"
|
||||||
:key="component.type"
|
:key="'recent-' + component.type"
|
||||||
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
|
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"
|
||||||
:class="{ 'hidden': !matchesSearch(component) }"
|
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, component)"
|
@dragstart="onDragStart($event, component)"
|
||||||
@dragend="onDragEnd($event)"
|
@dragend="onDragEnd($event)"
|
||||||
@ -32,106 +114,292 @@
|
|||||||
>
|
>
|
||||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selection & Toggle Category -->
|
<!-- Selection & Toggle Category -->
|
||||||
<div class="component-category mb-4">
|
<div class="component-category mb-4" v-if="shouldShowCategory('Selection Inputs')">
|
||||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Selection & Toggle</h3>
|
<div
|
||||||
<div class="grid grid-cols-2 gap-1 px-2">
|
@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
|
<div
|
||||||
v-for="component in getComponentsByCategory('Selection Inputs')"
|
v-for="component in getComponentsByCategory('Selection Inputs')"
|
||||||
:key="component.type"
|
:key="component.type"
|
||||||
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
|
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) }"
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, component)"
|
@dragstart="onDragStart($event, component)"
|
||||||
@dragend="onDragEnd($event)"
|
@dragend="onDragEnd($event)"
|
||||||
@click="addComponent(component)"
|
@click="addComponent(component)"
|
||||||
|
@mouseenter="showTooltip($event, component)"
|
||||||
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
<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>
|
<span
|
||||||
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||||
|
v-html="getHighlightedName(component)"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Date & Pickers Category -->
|
<!-- Date & Pickers Category -->
|
||||||
<div class="component-category mb-4">
|
<div class="component-category mb-4" v-if="shouldShowCategory('Date and Time')">
|
||||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Date & Pickers</h3>
|
<div
|
||||||
<div class="grid grid-cols-2 gap-1 px-2">
|
@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
|
<div
|
||||||
v-for="component in getComponentsByCategory('Date and Time')"
|
v-for="component in getComponentsByCategory('Date and Time')"
|
||||||
:key="component.type"
|
:key="component.type"
|
||||||
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
|
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) }"
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, component)"
|
@dragstart="onDragStart($event, component)"
|
||||||
@dragend="onDragEnd($event)"
|
@dragend="onDragEnd($event)"
|
||||||
@click="addComponent(component)"
|
@click="addComponent(component)"
|
||||||
|
@mouseenter="showTooltip($event, component)"
|
||||||
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
<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>
|
<span
|
||||||
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||||
|
v-html="getHighlightedName(component)"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Advanced & Upload Category -->
|
<!-- Advanced & Upload Category -->
|
||||||
<div class="component-category mb-4">
|
<div class="component-category mb-4" v-if="shouldShowCategory('Advanced')">
|
||||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Advanced & Upload</h3>
|
<div
|
||||||
<div class="grid grid-cols-2 gap-1 px-2">
|
@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
|
<div
|
||||||
v-for="component in getComponentsByCategory('Advanced')"
|
v-for="component in getComponentsByCategory('Advanced')"
|
||||||
:key="component.type"
|
:key="component.type"
|
||||||
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
|
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) }"
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, component)"
|
@dragstart="onDragStart($event, component)"
|
||||||
@dragend="onDragEnd($event)"
|
@dragend="onDragEnd($event)"
|
||||||
@click="addComponent(component)"
|
@click="addComponent(component)"
|
||||||
|
@mouseenter="showTooltip($event, component)"
|
||||||
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
<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>
|
<span
|
||||||
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||||
|
v-html="getHighlightedName(component)"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Layout & Content Category -->
|
<!-- Layout & Content Category -->
|
||||||
<div class="component-category mb-4">
|
<div class="component-category mb-4" v-if="shouldShowCategory('Layout')">
|
||||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Layout & Content</h3>
|
<div
|
||||||
<div class="grid grid-cols-2 gap-1 px-2">
|
@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
|
<div
|
||||||
v-for="component in getComponentsByCategory('Layout')"
|
v-for="component in getComponentsByCategory('Layout')"
|
||||||
:key="component.type"
|
:key="component.type"
|
||||||
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
|
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) }"
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, component)"
|
@dragstart="onDragStart($event, component)"
|
||||||
@dragend="onDragEnd($event)"
|
@dragend="onDragEnd($event)"
|
||||||
@click="addComponent(component)"
|
@click="addComponent(component)"
|
||||||
|
@mouseenter="showTooltip($event, component)"
|
||||||
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
<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>
|
<span
|
||||||
|
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||||
|
v-html="getHighlightedName(component)"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom HTML Category -->
|
<!-- Custom HTML Category -->
|
||||||
<div class="component-category mb-4">
|
<div class="component-category mb-4" v-if="shouldShowCategory('Custom HTML')">
|
||||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Custom HTML</h3>
|
<div
|
||||||
<div class="grid grid-cols-2 gap-1 px-2">
|
@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
|
<div
|
||||||
v-for="component in getComponentsByCategory('Custom HTML')"
|
v-for="component in getComponentsByCategory('Custom HTML')"
|
||||||
:key="component.type"
|
:key="component.type"
|
||||||
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
|
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) }"
|
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, component)"
|
@dragstart="onDragStart($event, component)"
|
||||||
@dragend="onDragEnd($event)"
|
@dragend="onDragEnd($event)"
|
||||||
@click="addComponent(component)"
|
@click="addComponent(component)"
|
||||||
|
@mouseenter="showTooltip($event, component)"
|
||||||
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@ -140,7 +408,31 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['add-component']);
|
const emit = defineEmits(['add-component']);
|
||||||
|
|
||||||
|
// Search and filtering state
|
||||||
const searchQuery = ref('');
|
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
|
// Define available form components
|
||||||
const availableComponents = [
|
const availableComponents = [
|
||||||
@ -1431,22 +1723,227 @@ fileInput.addEventListener('change', (e) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Get all available categories
|
||||||
|
const availableCategories = computed(() => {
|
||||||
|
return [...new Set(availableComponents.map(component => component.category))];
|
||||||
|
});
|
||||||
|
|
||||||
// Get components by category for rendering in sections
|
// Get components by category for rendering in sections
|
||||||
const getComponentsByCategory = (category) => {
|
const getComponentsByCategory = (category) => {
|
||||||
return availableComponents.filter(component => component.category === category);
|
return availableComponents.filter(component => component.category === category);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if component matches search query
|
// 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) => {
|
const matchesSearch = (component) => {
|
||||||
if (!searchQuery.value) return true;
|
// 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 query = searchQuery.value.toLowerCase();
|
||||||
return (
|
const matchesText = (
|
||||||
component.name.toLowerCase().includes(query) ||
|
component.name.toLowerCase().includes(query) ||
|
||||||
component.description.toLowerCase().includes(query) ||
|
component.description.toLowerCase().includes(query) ||
|
||||||
component.type.toLowerCase().includes(query) ||
|
component.type.toLowerCase().includes(query) ||
|
||||||
component.category.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
|
// Handle drag start event
|
||||||
@ -1511,6 +2008,7 @@ const onDragStart = (event, component) => {
|
|||||||
|
|
||||||
// Add a component directly via click
|
// Add a component directly via click
|
||||||
const addComponent = (component) => {
|
const addComponent = (component) => {
|
||||||
|
addToRecentlyUsed(component);
|
||||||
emit('add-component', component);
|
emit('add-component', component);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1527,9 +2025,7 @@ const onDragEnd = (event) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.form-components {
|
.form-components {
|
||||||
@apply bg-white h-full;
|
@apply bg-white h-full relative;
|
||||||
|
|
||||||
/* More compact layout */
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1538,13 +2034,33 @@ const onDragEnd = (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.component-category h3 {
|
.component-category h3 {
|
||||||
@apply text-xs font-semibold px-3 mb-2 uppercase tracking-wider text-gray-700;
|
@apply text-xs font-semibold uppercase tracking-wider text-gray-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Enhanced component items */
|
||||||
.component-item {
|
.component-item {
|
||||||
@apply h-20;
|
@apply h-20;
|
||||||
transition: all 0.15s ease-in-out;
|
transition: all 0.15s ease-in-out;
|
||||||
min-height: 4.5rem;
|
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 {
|
.component-item .iconify {
|
||||||
@ -1555,10 +2071,6 @@ const onDragEnd = (event) => {
|
|||||||
@apply leading-tight;
|
@apply leading-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-item:active {
|
.component-item:active {
|
||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
}
|
}
|
||||||
@ -1570,8 +2082,120 @@ const onDragEnd = (event) => {
|
|||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.3);
|
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 {
|
.drag-preview {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 9999;
|
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>
|
</style>
|
Loading…
x
Reference in New Issue
Block a user