Enhance Icon Selection and External URL Handling in Form Builder Components
- Updated FormBuilderFieldSettingsModal.vue and sideMenuNested.vue to replace text input for icon selection with an IconBrowser component, improving user experience for selecting Material Design icons. - Enhanced CustomActionButton.vue to differentiate between internal and external URLs when navigating, ensuring proper handling of external links. - Modified settings.local.json to include additional WebFetch commands for improved documentation access during development.
This commit is contained in:
parent
9c6f1ef666
commit
2562ebbc74
@ -5,7 +5,10 @@
|
|||||||
"Bash(yarn dev:*)",
|
"Bash(yarn dev:*)",
|
||||||
"Bash(yarn lint:*)",
|
"Bash(yarn lint:*)",
|
||||||
"Bash(yarn build:*)",
|
"Bash(yarn build:*)",
|
||||||
"Bash(npx eslint:*)"
|
"Bash(npx eslint:*)",
|
||||||
|
"WebFetch(domain:docs.iconify.design)",
|
||||||
|
"WebFetch(domain:iconify.design)",
|
||||||
|
"WebFetch(domain:api.iconify.design)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
@ -140,12 +140,23 @@ const handleClick = async (event) => {
|
|||||||
// Handle navigation based on target type
|
// Handle navigation based on target type
|
||||||
const openInNewTab = props.action.target === '_blank' || props.action.openInNewTab;
|
const openInNewTab = props.action.target === '_blank' || props.action.openInNewTab;
|
||||||
|
|
||||||
|
// Check if URL is external (has protocol or starts with //)
|
||||||
|
const isExternalUrl = (url) => {
|
||||||
|
return /^https?:\/\//.test(url) || /^\/\//.test(url) || !url.startsWith('/');
|
||||||
|
};
|
||||||
|
|
||||||
if (openInNewTab) {
|
if (openInNewTab) {
|
||||||
window.open(finalUrl, '_blank', 'noopener,noreferrer');
|
window.open(finalUrl, '_blank', 'noopener,noreferrer');
|
||||||
} else {
|
} else {
|
||||||
// Use navigateTo for SPA navigation if available, otherwise use window.location
|
// Use navigateTo for SPA navigation if available, otherwise use window.location
|
||||||
if (typeof navigateTo === 'function') {
|
if (typeof navigateTo === 'function') {
|
||||||
|
if (isExternalUrl(finalUrl)) {
|
||||||
|
// For external URLs, use navigateTo with external: true option
|
||||||
|
await navigateTo(finalUrl, { external: true });
|
||||||
|
} else {
|
||||||
|
// For internal URLs, use regular navigateTo
|
||||||
await navigateTo(finalUrl);
|
await navigateTo(finalUrl);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.location.href = finalUrl;
|
window.location.href = finalUrl;
|
||||||
}
|
}
|
||||||
|
@ -598,15 +598,18 @@
|
|||||||
:classes="{ outer: 'field-wrapper' }"
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<div class="field-wrapper">
|
||||||
type="text"
|
<label class="block text-sm font-medium text-gray-700 mb-1">Icon (Optional)</label>
|
||||||
label="Icon (Optional)"
|
<IconBrowser
|
||||||
name="icon"
|
|
||||||
v-model="configModel.icon"
|
v-model="configModel.icon"
|
||||||
help="Icon name (e.g., 'material-symbols:add')"
|
size="compact"
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
:show-preview="false"
|
||||||
placeholder="material-symbols:add"
|
placeholder="Search Material Design icons..."
|
||||||
/>
|
/>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">
|
||||||
|
Icon displayed on the button
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
@ -1328,14 +1331,20 @@ if (name && email) {
|
|||||||
:classes="{ outer: 'field-wrapper' }"
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<div class="field-wrapper">
|
||||||
type="text"
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
label="Icon (Optional)"
|
Icon (Optional)
|
||||||
|
</label>
|
||||||
|
<IconBrowser
|
||||||
v-model="action.icon"
|
v-model="action.icon"
|
||||||
placeholder="e.g., heroicons:eye, material-symbols:edit"
|
placeholder="Search Material Design icons..."
|
||||||
help="Leave empty for automatic icon based on button style"
|
size="compact"
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
:show-preview="false"
|
||||||
/>
|
/>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">
|
||||||
|
Leave empty for automatic icon based on button style
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
type="select"
|
type="select"
|
||||||
@ -2576,19 +2585,20 @@ if (this.element.querySelector('.file-upload')) {
|
|||||||
:classes="{ outer: 'mb-0' }"
|
:classes="{ outer: 'mb-0' }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<div class="mb-0">
|
||||||
type="text"
|
<label class="block text-sm font-medium text-gray-700 mb-1">Icon (optional)</label>
|
||||||
label="Icon (optional)"
|
<IconBrowser
|
||||||
v-model="field.icon"
|
v-model="field.icon"
|
||||||
placeholder="material-symbols:link"
|
size="compact"
|
||||||
help="Icon name to display"
|
:show-preview="false"
|
||||||
:classes="{ outer: 'mb-0' }"
|
placeholder="Search icons..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-else class="empty-fields-state text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
|
<div v-else class="empty-fields-state text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
|
||||||
@ -3076,6 +3086,7 @@ if (this.element.querySelector('.file-upload')) {
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import ValidationRulesHelp from '~/components/ValidationRulesHelp.vue'
|
import ValidationRulesHelp from '~/components/ValidationRulesHelp.vue'
|
||||||
|
import IconBrowser from '~/components/IconBrowser.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: Boolean,
|
modelValue: Boolean,
|
||||||
|
459
components/IconBrowser.vue
Normal file
459
components/IconBrowser.vue
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
<template>
|
||||||
|
<div class="icon-browser">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="search-container">
|
||||||
|
<div class="relative">
|
||||||
|
<Icon name="heroicons:magnifying-glass" class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
:placeholder="placeholder || 'Search icons...'"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:border-purple-500 focus:ring focus:ring-purple-200 text-sm"
|
||||||
|
@input="debouncedSearch"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="searchQuery"
|
||||||
|
@click="clearSearch"
|
||||||
|
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
title="Clear search"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" :class="gridClasses">
|
||||||
|
<div v-for="i in skeletonCount" :key="`skeleton-${i}`" class="icon-item skeleton">
|
||||||
|
<div class="icon-preview-skeleton"></div>
|
||||||
|
<div v-if="size !== 'compact'" class="icon-name-skeleton"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Icon Grid -->
|
||||||
|
<div v-else-if="icons.length > 0" :class="gridClasses">
|
||||||
|
<div
|
||||||
|
v-for="icon in displayIcons"
|
||||||
|
:key="icon"
|
||||||
|
class="icon-item"
|
||||||
|
:class="{ 'selected': modelValue === icon }"
|
||||||
|
@click="selectIcon(icon)"
|
||||||
|
:title="icon"
|
||||||
|
>
|
||||||
|
<div class="icon-preview">
|
||||||
|
<Icon :name="icon" :class="iconSizeClass" />
|
||||||
|
<div v-if="modelValue === icon" class="selected-overlay">
|
||||||
|
<Icon name="heroicons:check" class="w-3 h-3 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="size !== 'compact'" class="icon-name">{{ getIconDisplayName(icon) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else-if="searchQuery && !loading" class="empty-state">
|
||||||
|
<Icon name="material-symbols:search-off" class="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No icons found</h3>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
No icons match your search "{{ searchQuery }}".
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Try searching for: home, edit, delete, add, settings, arrow, check
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default State -->
|
||||||
|
<div v-else-if="!searchQuery && !loading" class="default-state">
|
||||||
|
<Icon name="material-symbols:palette" class="w-12 h-12 text-purple-400 mx-auto mb-3" />
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Choose an Icon</h3>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
Search from over 14,000 Material Design icons.
|
||||||
|
</p>
|
||||||
|
<div class="popular-searches">
|
||||||
|
<p class="text-sm font-medium text-gray-700 mb-2">Popular searches:</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="term in popularSearches"
|
||||||
|
:key="term"
|
||||||
|
@click="searchQuery = term; debouncedSearch()"
|
||||||
|
class="px-3 py-1 text-sm bg-purple-100 text-purple-700 rounded-full hover:bg-purple-200 transition-colors"
|
||||||
|
>
|
||||||
|
{{ term }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Icon Preview -->
|
||||||
|
<div v-if="modelValue && showPreview" class="selected-preview">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span class="text-sm font-medium text-gray-700">Selected Icon:</span>
|
||||||
|
<button
|
||||||
|
@click="clearSelection"
|
||||||
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
title="Clear selection"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-content">
|
||||||
|
<div class="preview-icon">
|
||||||
|
<Icon :name="modelValue" class="w-8 h-8 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="preview-details">
|
||||||
|
<code class="text-sm text-purple-600 bg-purple-50 px-2 py-1 rounded">{{ modelValue }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-if="hasError" class="error-state">
|
||||||
|
<div class="flex items-center text-red-600">
|
||||||
|
<Icon name="material-symbols:error" class="w-4 h-4 mr-2" />
|
||||||
|
<span class="text-sm">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Search icons...'
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'normal', // 'compact' | 'normal' | 'large'
|
||||||
|
validator: (value) => ['compact', 'normal', 'large'].includes(value)
|
||||||
|
},
|
||||||
|
showPreview: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
// Reactive state
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const icons = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
// Popular search terms for quick access
|
||||||
|
const popularSearches = ref([
|
||||||
|
'home', 'edit', 'delete', 'add', 'search', 'settings',
|
||||||
|
'arrow', 'check', 'close', 'menu', 'play', 'star'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const hasError = computed(() => {
|
||||||
|
if (props.required && !props.modelValue) return true;
|
||||||
|
return !!error.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMessage = computed(() => {
|
||||||
|
if (props.required && !props.modelValue) {
|
||||||
|
return 'Icon selection is required';
|
||||||
|
}
|
||||||
|
return error.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridClasses = computed(() => {
|
||||||
|
const baseClass = 'icon-grid';
|
||||||
|
const sizeClasses = {
|
||||||
|
compact: 'icon-grid-compact',
|
||||||
|
normal: 'icon-grid-normal',
|
||||||
|
large: 'icon-grid-large'
|
||||||
|
};
|
||||||
|
return `${baseClass} ${sizeClasses[props.size]}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconSizeClass = computed(() => {
|
||||||
|
const sizeClasses = {
|
||||||
|
compact: 'w-5 h-5',
|
||||||
|
normal: 'w-6 h-6',
|
||||||
|
large: 'w-8 h-8'
|
||||||
|
};
|
||||||
|
return sizeClasses[props.size];
|
||||||
|
});
|
||||||
|
|
||||||
|
const skeletonCount = computed(() => {
|
||||||
|
const counts = {
|
||||||
|
compact: 12,
|
||||||
|
normal: 24,
|
||||||
|
large: 16
|
||||||
|
};
|
||||||
|
return counts[props.size];
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayIcons = computed(() => {
|
||||||
|
const limits = {
|
||||||
|
compact: 12,
|
||||||
|
normal: 48,
|
||||||
|
large: 32
|
||||||
|
};
|
||||||
|
return icons.value.slice(0, limits[props.size]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const searchIcons = async (query) => {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
icons.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.iconify.design/search?query=${encodeURIComponent(query)}&prefix=material-symbols&limit=48`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
icons.value = data.icons || [];
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching icons:', err);
|
||||||
|
error.value = 'Failed to load icons. Please try again.';
|
||||||
|
icons.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounced search function
|
||||||
|
const debouncedSearch = useDebounceFn(() => {
|
||||||
|
searchIcons(searchQuery.value);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const selectIcon = (iconName) => {
|
||||||
|
if (props.disabled) return;
|
||||||
|
emit('update:modelValue', iconName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
emit('update:modelValue', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
icons.value = [];
|
||||||
|
error.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconDisplayName = (fullIconName) => {
|
||||||
|
// Remove the "material-symbols:" prefix for display
|
||||||
|
return fullIconName.replace('material-symbols:', '').replace(/-/g, ' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load default popular icons on component mount
|
||||||
|
const loadPopularIcons = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const popularTerms = ['home', 'edit', 'delete', 'add', 'search', 'settings'];
|
||||||
|
const allIcons = [];
|
||||||
|
|
||||||
|
for (const term of popularTerms) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.iconify.design/search?query=${term}&prefix=material-symbols&limit=8`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
allIcons.push(...(data.icons || []).slice(0, 4)); // Take first 4 from each category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates and limit to 24 icons
|
||||||
|
icons.value = [...new Set(allIcons)].slice(0, 24);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading popular icons:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load popular icons on mount
|
||||||
|
loadPopularIcons();
|
||||||
|
|
||||||
|
// Watch for external modelValue changes
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
// If external value changes, we might want to do something here
|
||||||
|
// For now, just ensure the error state is updated
|
||||||
|
error.value = '';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.icon-browser {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid {
|
||||||
|
@apply border border-gray-200 rounded-md bg-gray-50 overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-compact {
|
||||||
|
@apply grid grid-cols-6 gap-1 max-h-32 p-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-normal {
|
||||||
|
@apply grid grid-cols-6 gap-3 max-h-80 p-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-large {
|
||||||
|
@apply grid grid-cols-4 gap-4 max-h-96 p-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item {
|
||||||
|
@apply flex flex-col items-center rounded-md cursor-pointer hover:bg-white hover:shadow-sm transition-all duration-200 border border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-compact .icon-item {
|
||||||
|
@apply p-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-normal .icon-item {
|
||||||
|
@apply p-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-large .icon-item {
|
||||||
|
@apply p-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item.selected {
|
||||||
|
@apply bg-purple-50 border-purple-200 shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item.skeleton {
|
||||||
|
@apply cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-preview {
|
||||||
|
@apply relative flex items-center justify-center mb-1 rounded-md bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-compact .icon-preview {
|
||||||
|
@apply w-8 h-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-normal .icon-preview {
|
||||||
|
@apply w-10 h-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-large .icon-preview {
|
||||||
|
@apply w-12 h-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item:hover .icon-preview {
|
||||||
|
@apply bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item.selected .icon-preview {
|
||||||
|
@apply bg-purple-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-overlay {
|
||||||
|
@apply absolute -top-1 -right-1 w-5 h-5 bg-purple-600 rounded-full flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-name {
|
||||||
|
@apply text-xs text-gray-600 text-center leading-tight max-w-full overflow-hidden;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-item.selected .icon-name {
|
||||||
|
@apply text-purple-700 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton Loading */
|
||||||
|
.icon-preview-skeleton {
|
||||||
|
@apply w-10 h-10 bg-gray-200 rounded-md animate-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-name-skeleton {
|
||||||
|
@apply w-16 h-3 bg-gray-200 rounded animate-pulse mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* States */
|
||||||
|
.empty-state,
|
||||||
|
.default-state {
|
||||||
|
@apply text-center py-12 px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popular-searches {
|
||||||
|
@apply mt-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-preview {
|
||||||
|
@apply mt-4 p-3 bg-gray-50 border border-gray-200 rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
@apply flex items-center justify-between mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
@apply flex items-center space-x-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-icon {
|
||||||
|
@apply flex items-center justify-center w-12 h-12 bg-white rounded-md shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-details {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state {
|
||||||
|
@apply mt-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.icon-grid {
|
||||||
|
@apply grid-cols-4 gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-name {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.icon-grid {
|
||||||
|
@apply grid-cols-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import DraggableSideMenuNested from "~/components/draggable/sideMenuNested.vue";
|
import DraggableSideMenuNested from "~/components/draggable/sideMenuNested.vue";
|
||||||
|
import IconBrowser from "~/components/IconBrowser.vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
menus: {
|
menus: {
|
||||||
required: true,
|
required: true,
|
||||||
@ -467,17 +468,13 @@ const removeChild = (type, data) => {
|
|||||||
v-model="formMenu.path"
|
v-model="formMenu.path"
|
||||||
readonly
|
readonly
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit type="text" label="Icon" v-model="formMenu.icon"></FormKit>
|
<div class="mb-4">
|
||||||
<div class="mb-4 text-sm">
|
<label class="block text-sm font-medium text-gray-700 mb-2">Icon</label>
|
||||||
<p class="font-semibold mb-2">
|
<IconBrowser
|
||||||
Preview Icon (<a
|
v-model="formMenu.icon"
|
||||||
href="https://icones.js.org/collection/all"
|
size="compact"
|
||||||
class="text-primary hover:underline"
|
placeholder="Search Material Design icons..."
|
||||||
target="_blank"
|
/>
|
||||||
>https://icones.js.org/collection/all</a
|
|
||||||
>)
|
|
||||||
</p>
|
|
||||||
<Icon v-if="formMenu.icon" :name="formMenu.icon"></Icon>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="mb-4" />
|
<hr class="mb-4" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user