Add Tabler Icons and Refactor Form Components (#771)
- Updated `package.json` and `package-lock.json` to include `@iconify-json/tabler` for additional icon support. - Refactored `ImageInput.vue` to utilize `nuxt/icon` for icon rendering, enhancing consistency across components. - Introduced `OptionSelectorInput.vue` as a new form component for selecting options in a grid layout, integrating with the form system. - Updated `FormCustomization.vue` and `FormEditorPreview.vue` to utilize the new `OptionSelectorInput` for improved user experience in form settings. - Enhanced `HiddenRequiredDisabled.vue` to replace manual button rendering with `OptionSelectorInput`, streamlining the component structure. These changes aim to improve the iconography and form component functionality, providing a more cohesive and user-friendly interface.
This commit is contained in:
@@ -17,70 +17,84 @@
|
||||
:form="form"
|
||||
label="Form Theme"
|
||||
/>
|
||||
|
||||
<color-input
|
||||
name="color"
|
||||
:form="form"
|
||||
label="Accent Color"
|
||||
class="my-4"
|
||||
>
|
||||
<template #help>
|
||||
<InputHelp>
|
||||
<span class="text-gray-500">
|
||||
Color (for buttons & inputs border) - <a
|
||||
class="text-blue-500"
|
||||
href="#"
|
||||
@click.prevent="form.color = DEFAULT_COLOR"
|
||||
>Reset</a>
|
||||
</span>
|
||||
</InputHelp>
|
||||
<template #label>
|
||||
<InputLabel>Accent Color - <a
|
||||
href="#" class="text-blue-500"
|
||||
@click.prevent="form.color = DEFAULT_COLOR"
|
||||
>Reset</a></InputLabel>
|
||||
</template>
|
||||
</color-input>
|
||||
<select-input
|
||||
name="dark_mode"
|
||||
:options="[
|
||||
{ name: 'Auto', value: 'auto' },
|
||||
{ name: 'Light Mode', value: 'light' },
|
||||
{ name: 'Dark Mode', value: 'dark' },
|
||||
]"
|
||||
|
||||
<OptionSelectorInput
|
||||
v-model="form.dark_mode"
|
||||
:form="form"
|
||||
name="dark_mode"
|
||||
label="Color Mode"
|
||||
help="Use Auto to use device system preferences"
|
||||
:options="[
|
||||
{ name: 'auto', label: 'System', icon: 'i-heroicons-computer-desktop' },
|
||||
{ name: 'light', label: 'Light', icon: 'i-heroicons-sun' },
|
||||
{ name: 'dark', label: 'Dark', icon: 'i-heroicons-moon' },
|
||||
]"
|
||||
:multiple="false"
|
||||
:columns="3"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<EditorSectionHeader
|
||||
icon="octicon:typography-16"
|
||||
title="Typography"
|
||||
title="Text & Language"
|
||||
/>
|
||||
<template v-if="useFeatureFlag('services.google.fonts')">
|
||||
<label class="text-gray-700 font-medium text-sm">Font Style</label>
|
||||
<v-button
|
||||
color="white"
|
||||
class="w-full mb-4"
|
||||
size="small"
|
||||
@click="showGoogleFontPicker = true"
|
||||
>
|
||||
<span :style="{ 'font-family': (form.font_family?form.font_family+' !important':null) }">
|
||||
{{ form.font_family || 'Default' }}
|
||||
</span>
|
||||
</v-button>
|
||||
<GoogleFontPicker
|
||||
:show="showGoogleFontPicker"
|
||||
:font="form.font_family || null"
|
||||
@close="showGoogleFontPicker=false"
|
||||
@apply="onApplyFont"
|
||||
/>
|
||||
</template>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex-grow my-1" v-if="useFeatureFlag('services.google.fonts')">
|
||||
<label class="text-gray-700 font-semibold text-sm mb-1 block">Font Family</label>
|
||||
<v-button
|
||||
color="white"
|
||||
class="w-full py-1.5"
|
||||
size="small"
|
||||
@click="showGoogleFontPicker = true"
|
||||
>
|
||||
<span :style="{ 'font-family': (form.font_family ? form.font_family + ' !important' : null) }">
|
||||
{{ form.font_family || 'Default' }}
|
||||
</span>
|
||||
</v-button>
|
||||
<GoogleFontPicker
|
||||
:show="showGoogleFontPicker"
|
||||
:font="form.font_family || null"
|
||||
@close="showGoogleFontPicker = false"
|
||||
@apply="onApplyFont"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow">
|
||||
<select-input
|
||||
name="language"
|
||||
searchable
|
||||
:options="availableLocales"
|
||||
:form="form"
|
||||
label="Language"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToggleSwitchInput
|
||||
name="layout_rtl"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Right-to-Left Layout"
|
||||
/>
|
||||
|
||||
<toggle-switch-input
|
||||
name="uppercase_labels"
|
||||
:form="form"
|
||||
label="Uppercase Input Labels"
|
||||
/>
|
||||
|
||||
<select-input
|
||||
name="language"
|
||||
class="mt-4"
|
||||
searchable
|
||||
:options="availableLocales"
|
||||
:form="form"
|
||||
label="Form Language"
|
||||
label="Uppercase Input Labels"
|
||||
/>
|
||||
|
||||
<EditorSectionHeader
|
||||
@@ -88,74 +102,87 @@
|
||||
title="Layout & Sizing"
|
||||
/>
|
||||
<div class="flex space-x-4 justify-stretch">
|
||||
<select-input
|
||||
name="size"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'Small', value: 'sm' },
|
||||
{ name: 'Medium', value: 'md' },
|
||||
{ name: 'Large', value: 'lg' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Input Size"
|
||||
/>
|
||||
<div class="flex-grow">
|
||||
<OptionSelectorInput
|
||||
seamless
|
||||
label="Input Size"
|
||||
v-model="form.size"
|
||||
:form="form"
|
||||
name="size"
|
||||
:options="[
|
||||
{ name: 'sm', label:'S'},
|
||||
{ name: 'md', label:'M' },
|
||||
{ name: 'lg', label:'L' },
|
||||
]"
|
||||
:multiple="false"
|
||||
:columns="3"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select-input
|
||||
name="border_radius"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'None', value: 'none' },
|
||||
{ name: 'Small', value: 'small' },
|
||||
{ name: 'Full', value: 'full' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Input Roundness"
|
||||
/>
|
||||
<div class="flex-grow">
|
||||
<OptionSelectorInput
|
||||
label="Input Roundness"
|
||||
v-model="form.border_radius"
|
||||
seamless
|
||||
:form="form"
|
||||
name="border_radius"
|
||||
:options="[
|
||||
{ name: 'none', icon: 'i-tabler-border-corner-square' },
|
||||
{ name: 'small', icon: 'i-tabler-border-corner-rounded' },
|
||||
{ name: 'full', icon: 'i-tabler-border-corner-pill' },
|
||||
]"
|
||||
:multiple="false"
|
||||
:columns="3"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<select-input
|
||||
name="width"
|
||||
:options="[
|
||||
{ name: 'Centered', value: 'centered' },
|
||||
{ name: 'Full Width', value: 'full' },
|
||||
]"
|
||||
:form="form"
|
||||
|
||||
<OptionSelectorInput
|
||||
v-model="form.width"
|
||||
label="Form Width"
|
||||
help="Useful when embedding your form"
|
||||
/>
|
||||
|
||||
<ToggleSwitchInput
|
||||
name="layout_rtl"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Right-to-Left Layout"
|
||||
help="Adjusts layout for RTL languages"
|
||||
name="width"
|
||||
seamless
|
||||
:options="[
|
||||
{ name: 'centered', label: 'Centered' },
|
||||
{ name: 'full', label: 'Full Width' },
|
||||
]"
|
||||
:multiple="false"
|
||||
:columns="2"
|
||||
class="mb-4 w-2/3"
|
||||
/>
|
||||
|
||||
<EditorSectionHeader
|
||||
icon="heroicons:tag-16-solid"
|
||||
title="Branding & Media"
|
||||
/>
|
||||
<image-input
|
||||
name="logo_picture"
|
||||
:form="form"
|
||||
label="Logo"
|
||||
help="Not visible when form is embedded"
|
||||
:required="false"
|
||||
/>
|
||||
<image-input
|
||||
name="cover_picture"
|
||||
:form="form"
|
||||
label="Cover image"
|
||||
help="Not visible when form is embedded"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<image-input
|
||||
name="logo_picture"
|
||||
:form="form"
|
||||
label="Logo"
|
||||
:required="false"
|
||||
/>
|
||||
|
||||
<image-input
|
||||
name="cover_picture"
|
||||
:form="form"
|
||||
label="Cover (~1500px)"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<toggle-switch-input
|
||||
name="no_branding"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
@update:model-value="onChangeNoBranding"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-sm">
|
||||
Remove OpnForm Branding
|
||||
Hide OpnForm Branding
|
||||
</span>
|
||||
<pro-tag
|
||||
upgrade-modal-title="Upgrade today to remove OpnForm branding"
|
||||
@@ -182,7 +209,7 @@
|
||||
name="transparent_background"
|
||||
:form="form"
|
||||
label="Transparent Background"
|
||||
help="Only applies when form is embedded"
|
||||
help="When form is embedded"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="confetti_on_submission"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
class="border rounded-lg bg-white dark:bg-notion-dark w-full block shadow-sm transition-all flex flex-col"
|
||||
:class="{ 'max-w-5xl': !isExpanded, 'h-full': isExpanded }"
|
||||
>
|
||||
<div class="w-full bg-white dark:bg-gray-950 border-b border-gray-300 dark:border-blue-900 dark:border-gray-700 rounded-t-lg p-1.5 px-4 flex items-center gap-x-1.5">
|
||||
<div class="w-full bg-white dark:bg-gray-950 border-b border-gray-300 dark:border-blue-900 dark:border-gray-700 rounded-t-lg p-1.5 pl-4 pr-1.5 flex items-center gap-x-1.5">
|
||||
<div class="bg-red-500 rounded-full w-2.5 h-2.5" />
|
||||
<div class="bg-yellow-500 rounded-full w-2.5 h-2.5" />
|
||||
<div class="bg-green-500 rounded-full w-2.5 h-2.5" />
|
||||
|
||||
@@ -1,87 +1,61 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="option in availableOptions"
|
||||
:key="option.name"
|
||||
class="flex flex-col items-center justify-center p-1.5 border rounded-lg transition-colors text-gray-500"
|
||||
:class="[
|
||||
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option.name)) : option.class) : {},
|
||||
{
|
||||
'border-blue-500 bg-blue-50': isSelected(option.name),
|
||||
'hover:bg-gray-100 border-gray-300': !isSelected(option.name)
|
||||
}
|
||||
]"
|
||||
@click="toggleOption(option.name)"
|
||||
>
|
||||
<Icon
|
||||
:name="isSelected(option.name) && option.selectedIcon ? option.selectedIcon : option.icon"
|
||||
:class="[
|
||||
'w-4 h-4 mb-1',
|
||||
{
|
||||
'text-blue-500': isSelected(option.name),
|
||||
'text-inherit': !isSelected(option.name),
|
||||
},
|
||||
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option.name)) : option.iconClass) : {}
|
||||
]"
|
||||
/>
|
||||
<span
|
||||
class="text-xs"
|
||||
:class="{
|
||||
'text-blue-500': isSelected(option.name),
|
||||
'text-inherit': !isSelected(option.name),
|
||||
}"
|
||||
>{{ isSelected(option.name) ? option.selectedLabel ?? option.label : option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<OptionSelectorInput
|
||||
:options="availableOptions"
|
||||
v-model="selectedOption"
|
||||
:multiple="false"
|
||||
:disabled="false"
|
||||
:columns="3"
|
||||
name="field_state"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
canBeDisabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeRequired: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeHidden: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
canBeDisabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeRequired: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canBeHidden: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:field'])
|
||||
|
||||
defineEmits(['update:field'])
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
name: 'required',
|
||||
label: 'Required',
|
||||
icon: 'i-ph-asterisk-bold',
|
||||
selectedIcon: 'i-ph-asterisk-bold',
|
||||
const options = [
|
||||
{
|
||||
name: 'required',
|
||||
label: 'Required',
|
||||
icon: 'ph:asterisk-bold',
|
||||
selectedIcon: 'ph:asterisk-bold',
|
||||
iconClass: (isActive) => isActive ? 'text-red-500' : '',
|
||||
},
|
||||
{
|
||||
name: 'hidden',
|
||||
label: 'Hidden',
|
||||
icon: 'i-heroicons-eye',
|
||||
selectedIcon: 'i-heroicons-eye-slash-solid',
|
||||
{
|
||||
name: 'hidden',
|
||||
label: 'Hidden',
|
||||
icon: 'heroicons:eye',
|
||||
selectedIcon: 'heroicons:eye-slash-solid',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
label: 'Disabled',
|
||||
icon: 'i-heroicons-lock-open',
|
||||
selectedIcon: 'i-heroicons-lock-closed-solid',
|
||||
{
|
||||
name: 'disabled',
|
||||
label: 'Disabled',
|
||||
icon: 'heroicons:lock-open',
|
||||
selectedIcon: 'heroicons:lock-closed-solid',
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
const availableOptions = computed(() => {
|
||||
return options.value.filter(option => {
|
||||
return options.filter(option => {
|
||||
if (option.name === 'disabled') return props.canBeDisabled
|
||||
if (option.name === 'required') return props.canBeRequired
|
||||
if (option.name === 'hidden') return props.canBeHidden
|
||||
@@ -89,28 +63,34 @@ const availableOptions = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const isSelected = (optionName) => {
|
||||
return props.field[optionName]
|
||||
}
|
||||
|
||||
const toggleOption = (optionName) => {
|
||||
const newValue = !props.field[optionName]
|
||||
|
||||
if (optionName === 'required' && newValue) {
|
||||
props.field.hidden = false
|
||||
} else if (optionName === 'hidden' && newValue) {
|
||||
const selectedOption = computed({
|
||||
get() {
|
||||
// Only one can be true at a time, priority: required > hidden > disabled
|
||||
if (props.field.required) return 'required'
|
||||
if (props.field.hidden) return 'hidden'
|
||||
if (props.field.disabled) return 'disabled'
|
||||
return null
|
||||
},
|
||||
set(optionName) {
|
||||
// Reset all
|
||||
props.field.required = false
|
||||
props.field.disabled = false
|
||||
props.field.generates_uuid = false
|
||||
props.field.generates_auto_increment_id = false
|
||||
} else if (optionName === 'disabled' && newValue) {
|
||||
props.field.hidden = false
|
||||
props.field.disabled = false
|
||||
// Apply business logic
|
||||
if (optionName === 'required') {
|
||||
props.field.required = true
|
||||
props.field.hidden = false
|
||||
} else if (optionName === 'hidden') {
|
||||
props.field.hidden = true
|
||||
props.field.required = false
|
||||
props.field.disabled = false
|
||||
props.field.generates_uuid = false
|
||||
props.field.generates_auto_increment_id = false
|
||||
} else if (optionName === 'disabled') {
|
||||
props.field.disabled = true
|
||||
props.field.hidden = false
|
||||
}
|
||||
emit('update:field', { ...props.field })
|
||||
}
|
||||
|
||||
if ((optionName === 'disabled' && props.canBeDisabled) ||
|
||||
(optionName === 'required' && props.canBeRequired) ||
|
||||
(optionName === 'hidden' && props.canBeHidden)) {
|
||||
props.field[optionName] = newValue
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user