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:
parent
9a42aacc3a
commit
a140f789c2
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<input-wrapper v-bind="inputWrapperProps">
|
<InputWrapper v-bind="inputWrapperProps">
|
||||||
<template #label>
|
<template #label>
|
||||||
<slot name="label" />
|
<slot name="label" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<span class="inline-block w-full rounded-md shadow-sm">
|
<span class="inline-block w-full rounded-md shadow-xs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
aria-labelledby="listbox-label"
|
aria-labelledby="listbox-label"
|
||||||
class="cursor-pointer relative w-full"
|
class="cursor-pointer relative w-full"
|
||||||
:class="[
|
:class="[
|
||||||
theme.default.input,
|
theme.default.input,
|
||||||
theme.default.spacing.horizontal,
|
theme.default.spacing.horizontal,
|
||||||
theme.default.spacing.vertical,
|
theme.default.spacing.vertical,
|
||||||
theme.default.fontSize,
|
theme.default.fontSize,
|
||||||
|
|
@ -24,52 +24,37 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="currentUrl == null"
|
v-if="currentUrl == null"
|
||||||
class="text-gray-600 dark:text-gray-400"
|
class="text-gray-600 dark:text-gray-400 flex justify-center"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
name="heroicons:cloud-arrow-up"
|
||||||
class="h-5 w-5 inline"
|
class="h-5 w-5"
|
||||||
fill="none"
|
/>
|
||||||
viewBox="0 0 24 24"
|
<span class="ml-2">
|
||||||
stroke="currentColor"
|
Upload
|
||||||
>
|
</span>
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Upload image
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="h-6 text-gray-600 dark:text-gray-400 flex"
|
class=" text-gray-600 dark:text-gray-400 flex"
|
||||||
>
|
>
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<img
|
<img
|
||||||
:src="currentUrl"
|
:src="tmpFile ?? currentUrl"
|
||||||
class="h-6 rounded shadow-md"
|
class="h-5 rounded shadow-md border"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="hover:text-nt-blue flex"
|
class="text-gray-500 hover:text-red-500 flex items-center"
|
||||||
@click.prevent="clearUrl"
|
@click.prevent="clearUrl"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
name="heroicons:trash"
|
||||||
class="h-6 w-6"
|
class="h-5 w-5"
|
||||||
fill="none"
|
/>
|
||||||
viewBox="0 0 24 24"
|
</a>
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/></svg></a>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -105,7 +90,7 @@
|
||||||
v-if="loading"
|
v-if="loading"
|
||||||
class="text-gray-600 dark:text-gray-400"
|
class="text-gray-600 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<Loader class="h-6 w-6 mx-auto m-10" />
|
<loader class="h-5 w-5 mx-auto m-10" />
|
||||||
<p class="text-center mt-6">
|
<p class="text-center mt-6">
|
||||||
Uploading your file...
|
Uploading your file...
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -127,20 +112,10 @@
|
||||||
accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml"
|
accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml"
|
||||||
@change="manualFileUpload"
|
@change="manualFileUpload"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
name="heroicons:cloud-arrow-up"
|
||||||
class="mx-auto h-24 w-24 text-gray-200"
|
class="x-auto h-24 w-24 text-gray-200"
|
||||||
fill="none"
|
/>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<p class="mt-5 text-sm text-gray-600">
|
<p class="mt-5 text-sm text-gray-600">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -161,7 +136,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</input-wrapper>
|
</InputWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
<template>
|
||||||
|
<input-wrapper v-bind="inputWrapperProps">
|
||||||
|
<template #label>
|
||||||
|
<slot name="label" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid"
|
||||||
|
:class="[gridClass, { 'gap-2': !seamless }]"
|
||||||
|
:style="optionStyle"
|
||||||
|
role="listbox"
|
||||||
|
:aria-multiselectable="multiple ? 'true' : 'false'"
|
||||||
|
:tabindex="disabled ? -1 : 0"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
ref="root"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(option, idx) in options"
|
||||||
|
:key="option[optionKey]"
|
||||||
|
class="flex flex-col items-center justify-center p-1.5 border transition-colors text-gray-500 focus:outline-none"
|
||||||
|
:class="[
|
||||||
|
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option)) : option.class) : {},
|
||||||
|
{
|
||||||
|
'border-form-color text-form-color bg-form-color/10': isSelected(option),
|
||||||
|
'hover:bg-gray-100 border-gray-300': !isSelected(option),
|
||||||
|
'opacity-50 pointer-events-none': disabled || option.disabled,
|
||||||
|
// Seamless mode: only first and last have radius
|
||||||
|
'rounded-lg': !seamless,
|
||||||
|
'rounded-l-lg': seamless && idx === 0,
|
||||||
|
'rounded-r-lg': seamless && idx === options.length - 1,
|
||||||
|
// Seamless mode: overlap borders with negative margin, keep all borders
|
||||||
|
'-ml-px': seamless && idx > 0,
|
||||||
|
// Seamless mode: z-index hierarchy - selected > hovered/focused > default
|
||||||
|
'relative z-20': seamless && isSelected(option),
|
||||||
|
'relative z-10': seamless && !isSelected(option) && focusedIdx === idx,
|
||||||
|
'relative z-0': seamless && !isSelected(option) && focusedIdx !== idx,
|
||||||
|
// Add hover z-index for seamless mode (but lower than selected)
|
||||||
|
'hover:z-10': seamless && !isSelected(option)
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:aria-selected="isSelected(option) ? 'true' : 'false'"
|
||||||
|
:tabindex="disabled || option.disabled ? -1 : 0"
|
||||||
|
:disabled="disabled || option.disabled"
|
||||||
|
@click="selectOption(option)"
|
||||||
|
@focus="focusedIdx = idx"
|
||||||
|
@mouseenter="focusedIdx = idx"
|
||||||
|
:title="option.tooltip || ''"
|
||||||
|
role="option"
|
||||||
|
>
|
||||||
|
<slot name="icon" :option="option" :selected="isSelected(option)">
|
||||||
|
<Icon
|
||||||
|
v-if="option.icon"
|
||||||
|
:name="isSelected(option) && option.selectedIcon ? option.selectedIcon : option.icon"
|
||||||
|
:class="[
|
||||||
|
'w-4 h-4',
|
||||||
|
option.label ? 'mb-1' : '',
|
||||||
|
isSelected(option) ? 'text-form-color' : 'text-inherit',
|
||||||
|
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option)) : option.iconClass) : {}
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
<span
|
||||||
|
v-if="option.label || !option.icon"
|
||||||
|
class="text-xs"
|
||||||
|
:class="{
|
||||||
|
'text-form-color': isSelected(option),
|
||||||
|
'text-inherit': !isSelected(option),
|
||||||
|
}"
|
||||||
|
>{{ isSelected(option) ? option.selectedLabel ?? option.label : option.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #help>
|
||||||
|
<slot name="help" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #error>
|
||||||
|
<slot name="error" />
|
||||||
|
</template>
|
||||||
|
</input-wrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
|
import { inputProps, useFormInput } from './useFormInput.js'
|
||||||
|
import InputWrapper from './components/InputWrapper.vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OptionSelectorInput.vue
|
||||||
|
*
|
||||||
|
* A form input component for selecting options in a grid layout with icons.
|
||||||
|
* Integrates with the form system using InputWrapper and useFormInput.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - options: Array<{ name, label, icon, selectedIcon?, iconClass?, tooltip?, disabled? }>
|
||||||
|
* - multiple: Boolean (default: false)
|
||||||
|
* - optionKey: String (default: 'name')
|
||||||
|
* - columns: Number (default: 3, for grid layout)
|
||||||
|
* - seamless: Boolean (default: false, removes gaps and only applies radius to first/last items)
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Keyboard navigation (arrow keys, enter/space to select)
|
||||||
|
* - Focus management
|
||||||
|
* - Optional tooltips per option
|
||||||
|
* - Form validation integration
|
||||||
|
* - Notion-style look by default
|
||||||
|
* - Seamless mode for connected button appearance
|
||||||
|
*/
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
...inputProps,
|
||||||
|
options: { type: Array, required: true },
|
||||||
|
multiple: { type: Boolean, default: false },
|
||||||
|
optionKey: { type: String, default: 'name' },
|
||||||
|
columns: { type: Number, default: 3 },
|
||||||
|
seamless: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||||
|
|
||||||
|
// Use form input composable
|
||||||
|
const {
|
||||||
|
compVal,
|
||||||
|
inputWrapperProps
|
||||||
|
} = useFormInput(props, { emit })
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
const focusedIdx = ref(-1)
|
||||||
|
const root = ref(null)
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const gridClass = computed(() => `grid-cols-${props.columns}`)
|
||||||
|
|
||||||
|
const optionStyle = computed(() => ({
|
||||||
|
'--bg-form-color': props.color
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function isSelected(option) {
|
||||||
|
if (props.multiple) {
|
||||||
|
return Array.isArray(compVal.value) && compVal.value.includes(option[props.optionKey])
|
||||||
|
}
|
||||||
|
return compVal.value === option[props.optionKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectOption(option) {
|
||||||
|
if (props.disabled || option.disabled) return
|
||||||
|
|
||||||
|
if (props.multiple) {
|
||||||
|
let newValue = Array.isArray(compVal.value) ? [...compVal.value] : []
|
||||||
|
const idx = newValue.indexOf(option[props.optionKey])
|
||||||
|
if (idx > -1) {
|
||||||
|
newValue.splice(idx, 1)
|
||||||
|
} else {
|
||||||
|
newValue.push(option[props.optionKey])
|
||||||
|
}
|
||||||
|
compVal.value = newValue
|
||||||
|
} else {
|
||||||
|
compVal.value = isSelected(option) ? null : option[props.optionKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(e) {
|
||||||
|
if (props.disabled) return
|
||||||
|
const len = props.options.length
|
||||||
|
if (len === 0) return
|
||||||
|
|
||||||
|
if (["ArrowRight", "ArrowDown"].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
focusedIdx.value = (focusedIdx.value + 1) % len
|
||||||
|
focusButton(focusedIdx.value)
|
||||||
|
} else if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
focusedIdx.value = (focusedIdx.value - 1 + len) % len
|
||||||
|
focusButton(focusedIdx.value)
|
||||||
|
} else if (["Enter", " ", "Spacebar"].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (focusedIdx.value >= 0 && focusedIdx.value < len) {
|
||||||
|
selectOption(props.options[focusedIdx.value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusButton(idx) {
|
||||||
|
nextTick(() => {
|
||||||
|
const btns = root.value?.querySelectorAll('button')
|
||||||
|
if (btns && btns[idx]) btns[idx].focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watchers
|
||||||
|
watch(compVal, (val) => {
|
||||||
|
// Keep focus on selected
|
||||||
|
if (!props.multiple && val != null) {
|
||||||
|
const idx = props.options.findIndex(opt => opt[props.optionKey] === val)
|
||||||
|
if (idx !== -1) focusedIdx.value = idx
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -17,70 +17,84 @@
|
||||||
:form="form"
|
:form="form"
|
||||||
label="Form Theme"
|
label="Form Theme"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<color-input
|
<color-input
|
||||||
name="color"
|
name="color"
|
||||||
:form="form"
|
:form="form"
|
||||||
|
label="Accent Color"
|
||||||
|
class="my-4"
|
||||||
>
|
>
|
||||||
<template #help>
|
<template #label>
|
||||||
<InputHelp>
|
<InputLabel>Accent Color - <a
|
||||||
<span class="text-gray-500">
|
href="#" class="text-blue-500"
|
||||||
Color (for buttons & inputs border) - <a
|
@click.prevent="form.color = DEFAULT_COLOR"
|
||||||
class="text-blue-500"
|
>Reset</a></InputLabel>
|
||||||
href="#"
|
|
||||||
@click.prevent="form.color = DEFAULT_COLOR"
|
|
||||||
>Reset</a>
|
|
||||||
</span>
|
|
||||||
</InputHelp>
|
|
||||||
</template>
|
</template>
|
||||||
</color-input>
|
</color-input>
|
||||||
<select-input
|
|
||||||
name="dark_mode"
|
<OptionSelectorInput
|
||||||
:options="[
|
v-model="form.dark_mode"
|
||||||
{ name: 'Auto', value: 'auto' },
|
|
||||||
{ name: 'Light Mode', value: 'light' },
|
|
||||||
{ name: 'Dark Mode', value: 'dark' },
|
|
||||||
]"
|
|
||||||
:form="form"
|
:form="form"
|
||||||
|
name="dark_mode"
|
||||||
label="Color 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
|
<EditorSectionHeader
|
||||||
icon="octicon:typography-16"
|
icon="octicon:typography-16"
|
||||||
title="Typography"
|
title="Text & Language"
|
||||||
/>
|
/>
|
||||||
<template v-if="useFeatureFlag('services.google.fonts')">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<label class="text-gray-700 font-medium text-sm">Font Style</label>
|
<div class="flex-grow my-1" v-if="useFeatureFlag('services.google.fonts')">
|
||||||
<v-button
|
<label class="text-gray-700 font-semibold text-sm mb-1 block">Font Family</label>
|
||||||
color="white"
|
<v-button
|
||||||
class="w-full mb-4"
|
color="white"
|
||||||
size="small"
|
class="w-full py-1.5"
|
||||||
@click="showGoogleFontPicker = true"
|
size="small"
|
||||||
>
|
@click="showGoogleFontPicker = true"
|
||||||
<span :style="{ 'font-family': (form.font_family?form.font_family+' !important':null) }">
|
>
|
||||||
{{ form.font_family || 'Default' }}
|
<span :style="{ 'font-family': (form.font_family ? form.font_family + ' !important' : null) }">
|
||||||
</span>
|
{{ form.font_family || 'Default' }}
|
||||||
</v-button>
|
</span>
|
||||||
<GoogleFontPicker
|
</v-button>
|
||||||
:show="showGoogleFontPicker"
|
<GoogleFontPicker
|
||||||
:font="form.font_family || null"
|
:show="showGoogleFontPicker"
|
||||||
@close="showGoogleFontPicker=false"
|
:font="form.font_family || null"
|
||||||
@apply="onApplyFont"
|
@close="showGoogleFontPicker = false"
|
||||||
/>
|
@apply="onApplyFont"
|
||||||
</template>
|
/>
|
||||||
|
</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
|
<toggle-switch-input
|
||||||
name="uppercase_labels"
|
name="uppercase_labels"
|
||||||
:form="form"
|
:form="form"
|
||||||
label="Uppercase Input Labels"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<select-input
|
|
||||||
name="language"
|
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
searchable
|
label="Uppercase Input Labels"
|
||||||
:options="availableLocales"
|
|
||||||
:form="form"
|
|
||||||
label="Form Language"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditorSectionHeader
|
<EditorSectionHeader
|
||||||
|
|
@ -88,74 +102,87 @@
|
||||||
title="Layout & Sizing"
|
title="Layout & Sizing"
|
||||||
/>
|
/>
|
||||||
<div class="flex space-x-4 justify-stretch">
|
<div class="flex space-x-4 justify-stretch">
|
||||||
<select-input
|
<div class="flex-grow">
|
||||||
name="size"
|
<OptionSelectorInput
|
||||||
class="flex-grow"
|
seamless
|
||||||
:options="[
|
label="Input Size"
|
||||||
{ name: 'Small', value: 'sm' },
|
v-model="form.size"
|
||||||
{ name: 'Medium', value: 'md' },
|
:form="form"
|
||||||
{ name: 'Large', value: 'lg' },
|
name="size"
|
||||||
]"
|
:options="[
|
||||||
:form="form"
|
{ name: 'sm', label:'S'},
|
||||||
label="Input Size"
|
{ name: 'md', label:'M' },
|
||||||
/>
|
{ name: 'lg', label:'L' },
|
||||||
|
]"
|
||||||
|
:multiple="false"
|
||||||
|
:columns="3"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<select-input
|
<div class="flex-grow">
|
||||||
name="border_radius"
|
<OptionSelectorInput
|
||||||
class="flex-grow"
|
label="Input Roundness"
|
||||||
:options="[
|
v-model="form.border_radius"
|
||||||
{ name: 'None', value: 'none' },
|
seamless
|
||||||
{ name: 'Small', value: 'small' },
|
:form="form"
|
||||||
{ name: 'Full', value: 'full' },
|
name="border_radius"
|
||||||
]"
|
:options="[
|
||||||
:form="form"
|
{ name: 'none', icon: 'i-tabler-border-corner-square' },
|
||||||
label="Input Roundness"
|
{ 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>
|
</div>
|
||||||
<select-input
|
|
||||||
name="width"
|
<OptionSelectorInput
|
||||||
:options="[
|
v-model="form.width"
|
||||||
{ name: 'Centered', value: 'centered' },
|
|
||||||
{ name: 'Full Width', value: 'full' },
|
|
||||||
]"
|
|
||||||
:form="form"
|
|
||||||
label="Form Width"
|
label="Form Width"
|
||||||
help="Useful when embedding your form"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ToggleSwitchInput
|
|
||||||
name="layout_rtl"
|
|
||||||
:form="form"
|
:form="form"
|
||||||
class="mt-4"
|
name="width"
|
||||||
label="Right-to-Left Layout"
|
seamless
|
||||||
help="Adjusts layout for RTL languages"
|
:options="[
|
||||||
|
{ name: 'centered', label: 'Centered' },
|
||||||
|
{ name: 'full', label: 'Full Width' },
|
||||||
|
]"
|
||||||
|
:multiple="false"
|
||||||
|
:columns="2"
|
||||||
|
class="mb-4 w-2/3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditorSectionHeader
|
<EditorSectionHeader
|
||||||
icon="heroicons:tag-16-solid"
|
icon="heroicons:tag-16-solid"
|
||||||
title="Branding & Media"
|
title="Branding & Media"
|
||||||
/>
|
/>
|
||||||
<image-input
|
<div class="grid grid-cols-2 gap-4">
|
||||||
name="logo_picture"
|
<image-input
|
||||||
:form="form"
|
name="logo_picture"
|
||||||
label="Logo"
|
:form="form"
|
||||||
help="Not visible when form is embedded"
|
label="Logo"
|
||||||
:required="false"
|
:required="false"
|
||||||
/>
|
/>
|
||||||
<image-input
|
|
||||||
name="cover_picture"
|
<image-input
|
||||||
:form="form"
|
name="cover_picture"
|
||||||
label="Cover image"
|
:form="form"
|
||||||
help="Not visible when form is embedded"
|
label="Cover (~1500px)"
|
||||||
/>
|
:required="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<toggle-switch-input
|
<toggle-switch-input
|
||||||
name="no_branding"
|
name="no_branding"
|
||||||
:form="form"
|
:form="form"
|
||||||
|
class="mt-4"
|
||||||
@update:model-value="onChangeNoBranding"
|
@update:model-value="onChangeNoBranding"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
Remove OpnForm Branding
|
Hide OpnForm Branding
|
||||||
</span>
|
</span>
|
||||||
<pro-tag
|
<pro-tag
|
||||||
upgrade-modal-title="Upgrade today to remove OpnForm branding"
|
upgrade-modal-title="Upgrade today to remove OpnForm branding"
|
||||||
|
|
@ -182,7 +209,7 @@
|
||||||
name="transparent_background"
|
name="transparent_background"
|
||||||
:form="form"
|
:form="form"
|
||||||
label="Transparent Background"
|
label="Transparent Background"
|
||||||
help="Only applies when form is embedded"
|
help="When form is embedded"
|
||||||
/>
|
/>
|
||||||
<toggle-switch-input
|
<toggle-switch-input
|
||||||
name="confetti_on_submission"
|
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="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 }"
|
: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-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-yellow-500 rounded-full w-2.5 h-2.5" />
|
||||||
<div class="bg-green-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>
|
<template>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<OptionSelectorInput
|
||||||
<button
|
:options="availableOptions"
|
||||||
v-for="option in availableOptions"
|
v-model="selectedOption"
|
||||||
:key="option.name"
|
:multiple="false"
|
||||||
class="flex flex-col items-center justify-center p-1.5 border rounded-lg transition-colors text-gray-500"
|
:disabled="false"
|
||||||
:class="[
|
:columns="3"
|
||||||
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option.name)) : option.class) : {},
|
name="field_state"
|
||||||
{
|
/>
|
||||||
'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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
canBeDisabled: {
|
canBeDisabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
canBeRequired: {
|
canBeRequired: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
canBeHidden: {
|
canBeHidden: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const emit = defineEmits(['update:field'])
|
||||||
|
|
||||||
defineEmits(['update:field'])
|
const options = [
|
||||||
|
{
|
||||||
const options = ref([
|
name: 'required',
|
||||||
{
|
label: 'Required',
|
||||||
name: 'required',
|
icon: 'ph:asterisk-bold',
|
||||||
label: 'Required',
|
selectedIcon: 'ph:asterisk-bold',
|
||||||
icon: 'i-ph-asterisk-bold',
|
|
||||||
selectedIcon: 'i-ph-asterisk-bold',
|
|
||||||
iconClass: (isActive) => isActive ? 'text-red-500' : '',
|
iconClass: (isActive) => isActive ? 'text-red-500' : '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'hidden',
|
name: 'hidden',
|
||||||
label: 'Hidden',
|
label: 'Hidden',
|
||||||
icon: 'i-heroicons-eye',
|
icon: 'heroicons:eye',
|
||||||
selectedIcon: 'i-heroicons-eye-slash-solid',
|
selectedIcon: 'heroicons:eye-slash-solid',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'disabled',
|
name: 'disabled',
|
||||||
label: 'Disabled',
|
label: 'Disabled',
|
||||||
icon: 'i-heroicons-lock-open',
|
icon: 'heroicons:lock-open',
|
||||||
selectedIcon: 'i-heroicons-lock-closed-solid',
|
selectedIcon: 'heroicons:lock-closed-solid',
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
|
|
||||||
const availableOptions = computed(() => {
|
const availableOptions = computed(() => {
|
||||||
return options.value.filter(option => {
|
return options.filter(option => {
|
||||||
if (option.name === 'disabled') return props.canBeDisabled
|
if (option.name === 'disabled') return props.canBeDisabled
|
||||||
if (option.name === 'required') return props.canBeRequired
|
if (option.name === 'required') return props.canBeRequired
|
||||||
if (option.name === 'hidden') return props.canBeHidden
|
if (option.name === 'hidden') return props.canBeHidden
|
||||||
|
|
@ -89,28 +63,34 @@ const availableOptions = computed(() => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const isSelected = (optionName) => {
|
const selectedOption = computed({
|
||||||
return props.field[optionName]
|
get() {
|
||||||
}
|
// Only one can be true at a time, priority: required > hidden > disabled
|
||||||
|
if (props.field.required) return 'required'
|
||||||
const toggleOption = (optionName) => {
|
if (props.field.hidden) return 'hidden'
|
||||||
const newValue = !props.field[optionName]
|
if (props.field.disabled) return 'disabled'
|
||||||
|
return null
|
||||||
if (optionName === 'required' && newValue) {
|
},
|
||||||
props.field.hidden = false
|
set(optionName) {
|
||||||
} else if (optionName === 'hidden' && newValue) {
|
// Reset all
|
||||||
props.field.required = false
|
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.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>
|
</script>
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
"@iconify-json/clarity": "^1.2.1",
|
"@iconify-json/clarity": "^1.2.1",
|
||||||
"@iconify-json/ic": "^1.2.1",
|
"@iconify-json/ic": "^1.2.1",
|
||||||
"@iconify-json/octicon": "^1.2.1",
|
"@iconify-json/octicon": "^1.2.1",
|
||||||
|
"@iconify-json/tabler": "^1.2.1",
|
||||||
"@nuxt/eslint-config": "^1.3.0",
|
"@nuxt/eslint-config": "^1.3.0",
|
||||||
"@nuxt/icon": "^1.12.0",
|
"@nuxt/icon": "^1.12.0",
|
||||||
"@nuxtjs/i18n": "^9.0.0",
|
"@nuxtjs/i18n": "^9.0.0",
|
||||||
|
|
@ -1588,6 +1589,15 @@
|
||||||
"@iconify/types": "*"
|
"@iconify/types": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify-json/tabler": {
|
||||||
|
"version": "1.2.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/tabler/-/tabler-1.2.18.tgz",
|
||||||
|
"integrity": "sha512-W+8qiJhJpb4dmBw3P7JSM9QhGsFG8GIS3BJWAmrJ/92rzK6NPGUOPfGmswoO+/MuPzQV96ColY9lcUktUKv0pg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@iconify/collections": {
|
"node_modules/@iconify/collections": {
|
||||||
"version": "1.0.546",
|
"version": "1.0.546",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.546.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.546.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"@iconify-json/clarity": "^1.2.1",
|
"@iconify-json/clarity": "^1.2.1",
|
||||||
"@iconify-json/ic": "^1.2.1",
|
"@iconify-json/ic": "^1.2.1",
|
||||||
"@iconify-json/octicon": "^1.2.1",
|
"@iconify-json/octicon": "^1.2.1",
|
||||||
|
"@iconify-json/tabler": "^1.2.1",
|
||||||
"@nuxt/eslint-config": "^1.3.0",
|
"@nuxt/eslint-config": "^1.3.0",
|
||||||
"@nuxt/icon": "^1.12.0",
|
"@nuxt/icon": "^1.12.0",
|
||||||
"@nuxtjs/i18n": "^9.0.0",
|
"@nuxtjs/i18n": "^9.0.0",
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ module.exports = {
|
||||||
border: 'rgba(15, 15, 15, 0.1)',
|
border: 'rgba(15, 15, 15, 0.1)',
|
||||||
borderDark: 'rgba(255, 255, 255, 0.1)'
|
borderDark: 'rgba(255, 255, 255, 0.1)'
|
||||||
},
|
},
|
||||||
"form-color": "var(--bg-form-color)",
|
'form-color': 'rgb(from var(--form-color, var(--bg-form-color)) r g b / <alpha-value>)'
|
||||||
},
|
},
|
||||||
transitionProperty: {
|
transitionProperty: {
|
||||||
height: "height",
|
height: "height",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue