Better form themes (#465)

* Working on custom radius + input size

* Fix date input clear vertical align

* Moslty finished implementing small size

* Polishing larger theme

* Finish large theme

* Added size/radius options in form editor

* Darken help text, improve switch input help location

* Slight form editor improvement

* Fix styling

* Polish of the form editor
This commit is contained in:
Julien Nahum 2024-06-27 17:52:49 +02:00 committed by GitHub
parent a84abf9f72
commit 2ca2d97e8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1058 additions and 494 deletions

View File

@ -31,6 +31,8 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
// Customization
'theme' => ['required', Rule::in(Form::THEMES)],
'width' => ['required', Rule::in(Form::WIDTHS)],
'size' => ['required', Rule::in(Form::SIZES)],
'border_radius' => ['required', Rule::in(Form::BORDER_RADIUS)],
'cover_picture' => 'url|nullable',
'logo_picture' => 'url|nullable',
'dark_mode' => ['required', Rule::in(Form::DARK_MODE_VALUES)],

View File

@ -30,6 +30,10 @@ class Form extends Model implements CachableAttributes
public const DARK_MODE_VALUES = ['auto', 'light', 'dark'];
public const SIZES = ['sm','md','lg'];
public const BORDER_RADIUS = ['none','small','full'];
public const THEMES = ['default', 'simple', 'notion'];
public const WIDTHS = ['centered', 'full'];
@ -49,6 +53,8 @@ class Form extends Model implements CachableAttributes
// Customization
'custom_domain',
'size',
'border_radius',
'theme',
'width',
'cover_picture',

View File

@ -10,9 +10,16 @@
:disabled="disabled ? true : null"
:name="name"
:color="color"
:theme="theme"
>
<slot name="label">
{{ label }}
<slot
name="label"
>
<span
:class="[
theme.SelectInput.fontSize,
]"
>{{ label }}</span>
<span
v-if="required"
class="text-red-500 required-dot"

View File

@ -11,6 +11,7 @@
<div
:class="[
theme.CodeInput.input,
theme.CodeInput.borderRadius,
{
'!ring-red-500 !ring-2 !border-transparent': hasError,
'!cursor-not-allowed !bg-gray-200': disabled,

View File

@ -10,18 +10,18 @@
:popper="{ placement: 'bottom-start' }"
>
<button
ref="datepicker"
class="cursor-pointer overflow-hidden"
:class="inputClasses"
:disabled="props.disabled"
ref="datepicker"
>
<div class="flex items-center min-w-0">
<div class="flex items-stretch min-w-0">
<div
class="flex-grow min-w-0 flex items-center gap-x-2"
:class="[
props.theme.default.inputSpacing.vertical,
props.theme.default.inputSpacing.horizontal,
{'hover:bg-gray-50 dark:hover:bg-gray-900': !props.disabled}
props.theme.DateInput.spacing.horizontal,
props.theme.DateInput.spacing.vertical,
props.theme.DateInput.fontSize,
]"
>
<Icon
@ -29,16 +29,24 @@
class="w-4 h-4 flex-shrink-0"
dynamic
/>
<div class="flex-grow truncate overflow-hidden">
<p class="flex-grow truncate h-[24px]">
<div class="flex-grow truncate overflow-hidden flex items-center">
<p
v-if="formattedDatePreview"
class="flex-grow truncate"
>
{{ formattedDatePreview }}
</p>
<p
v-else
class="text-transparent"
>
-
</p>
</div>
</div>
<button
v-if="fromDate && !props.disabled"
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2"
:class="[props.theme.default.inputSpacing.vertical]"
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2 flex items-center"
@click.prevent="clear()"
>
<Icon
@ -133,7 +141,7 @@ const modeledValue = computed({
})
const inputClasses = computed(() => {
const classes = [props.theme.DateInput.input, 'w-full']
const classes = [props.theme.DateInput.input, props.theme.DateInput.borderRadius]
if (props.disabled) {
classes.push('!cursor-not-allowed dark:!bg-gray-600 !bg-gray-200')
}

View File

@ -5,7 +5,10 @@
</template>
<div
v-if="cameraUpload && isInWebcam"
class="hidden sm:block w-full min-h-40"
class="hidden sm:block w-full"
:class="[
theme.fileInput.minHeight
]"
>
<camera-upload
v-if="cameraUpload"
@ -17,9 +20,17 @@
<div
v-else
class="flex flex-col w-full items-center justify-center transition-colors duration-40"
:class="[{'!cursor-not-allowed':disabled, 'cursor-pointer':!disabled,
[theme.fileInput.inputHover.light + ' dark:'+theme.fileInput.inputHover.dark]: uploadDragoverEvent,
['hover:'+theme.fileInput.inputHover.light +' dark:hover:'+theme.fileInput.inputHover.dark]: !loading}, theme.fileInput.input]"
:class="[
{'!cursor-not-allowed':disabled, 'cursor-pointer':!disabled,
[theme.fileInput.inputHover.light + ' dark:'+theme.fileInput.inputHover.dark]: uploadDragoverEvent,
['hover:'+theme.fileInput.inputHover.light +' dark:hover:'+theme.fileInput.inputHover.dark]: !loading},
theme.fileInput.input,
theme.fileInput.borderRadius,
theme.fileInput.spacing.horizontal,
theme.fileInput.spacing.vertical,
theme.fileInput.fontSize,
theme.fileInput.minHeight
]"
@dragover.prevent="uploadDragoverEvent=true"
@dragleave.prevent="uploadDragoverEvent=false"
@drop.prevent="onUploadDropEvent"
@ -66,7 +77,7 @@
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
class="w-5 h-5"
>
<path
stroke-linecap="round"
@ -76,7 +87,7 @@
</svg>
</div>
<p class="mt-2 text-sm text-gray-500 font-semibold select-none">
<p class="mt-2 text-sm text-gray-500 font-medium select-none">
Click to choose {{ multiple ? 'file(s)' : 'a file' }} or drag here
</p>
<p class="mt-1 text-xs text-gray-400 dark:text-gray-600 select-none">
@ -129,24 +140,24 @@
</template>
<script>
import { inputProps, useFormInput } from './useFormInput.js'
import {inputProps, useFormInput} from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue'
import UploadedFile from './components/UploadedFile.vue'
import OpenFormButton from '../open/forms/OpenFormButton.vue'
import CameraUpload from './components/CameraUpload.vue'
import { storeFile } from "~/lib/file-uploads.js"
import {storeFile} from "~/lib/file-uploads.js"
export default {
name: 'FileInput',
components: { InputWrapper, UploadedFile, OpenFormButton },
components: {InputWrapper, UploadedFile, OpenFormButton},
mixins: [],
props: {
...inputProps,
multiple: { type: Boolean, default: true },
cameraUpload: { type: Boolean, default: false },
mbLimit: { type: Number, default: 5 },
accept: { type: String, default: '' },
moveToFormAssets: { type: Boolean, default: false }
multiple: {type: Boolean, default: true},
cameraUpload: {type: Boolean, default: false},
mbLimit: {type: Number, default: 5},
accept: {type: String, default: ''},
moveToFormAssets: {type: Boolean, default: false}
},
setup(props, context) {
@ -159,7 +170,7 @@ export default {
files: [],
uploadDragoverEvent: false,
loading: false,
isInWebcam:false
isInWebcam: false
}),
computed: {
@ -254,13 +265,13 @@ export default {
this.uploadFileToServer(files.item(i))
}
},
openWebcam(){
if(!this.cameraUpload){
openWebcam() {
if (!this.cameraUpload) {
return
}
this.isInWebcam = true
},
cameraFileUpload(file){
cameraFileUpload(file) {
this.isInWebcam = false
this.isUploading = false
this.uploadFileToServer(file)

View File

@ -10,41 +10,74 @@
class="h-6 w-6 text-nt-blue mx-auto"
/>
<div
v-for="(option, index) in options"
v-else
:key="option[optionKey]"
role="button"
class="relative overflow-hidden"
:class="[
theme.default.input,
'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 flex',
theme.default.borderRadius,
{
'mb-2': index !== options.length,
'!ring-red-500 !ring-2 !border-transparent': hasError,
'!cursor-not-allowed !bg-gray-200': disabled,
},
]"
@click="onSelect(option[optionKey])"
>
<p class="flex-grow">
{{ option[displayKey] }}
</p>
<div
v-if="isSelected(option[optionKey])"
class="flex items-center"
v-for="(option, index) in options"
v-if="options && options.length"
:key="option[optionKey]"
:role="multiple?'checkbox':'radio'"
:aria-checked="isSelected(option[optionKey])"
:class="[
theme.FlatSelectInput.spacing.vertical,
theme.FlatSelectInput.fontSize,
theme.FlatSelectInput.option,
]"
@click="onSelect(option[optionKey])"
>
<svg
:color="color"
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
<template v-if="multiple">
<Icon
v-if="isSelected(option[optionKey])"
name="material-symbols:check-box"
class="text-inherit"
:color="color"
:class="[theme.FlatSelectInput.icon]"
/>
</svg>
<Icon
v-else
name="material-symbols:check-box-outline-blank"
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
/>
</template>
<template v-else>
<Icon
v-if="isSelected(option[optionKey])"
name="material-symbols:radio-button-checked-outline"
class="text-inherit"
:color="color"
:class="[theme.FlatSelectInput.icon]"
/>
<Icon
v-else
name="material-symbols:radio-button-unchecked"
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
/>
</template>
<p class="flex-grow">
{{ option[displayKey] }}
</p>
</div>
<div
v-else
:class="[
theme.FlatSelectInput.spacing.horizontal,
theme.FlatSelectInput.spacing.vertical,
theme.FlatSelectInput.fontSize,
theme.FlatSelectInput.option,
'!text-gray-500 !cursor-not-allowed'
]"
>
No options available.
</div>
</div>
@ -58,7 +91,7 @@
</template>
<script>
import { inputProps, useFormInput } from "./useFormInput.js"
import {inputProps, useFormInput} from "./useFormInput.js"
import InputWrapper from "./components/InputWrapper.vue"
/**
@ -66,16 +99,16 @@ import InputWrapper from "./components/InputWrapper.vue"
*/
export default {
name: "FlatSelectInput",
components: { InputWrapper },
components: {InputWrapper},
props: {
...inputProps,
options: { type: Array, required: true },
optionKey: { type: String, default: "value" },
emitKey: { type: String, default: "value" },
displayKey: { type: String, default: "name" },
loading: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
options: {type: Array, required: true},
optionKey: {type: String, default: "value"},
emitKey: {type: String, default: "value"},
displayKey: {type: String, default: "name"},
loading: {type: Boolean, default: false},
multiple: {type: Boolean, default: false},
},
setup(props, context) {
return {

View File

@ -11,18 +11,24 @@
aria-expanded="true"
aria-labelledby="listbox-label"
class="cursor-pointer relative w-full"
:class="[theme.default.input, { 'ring-red-500 ring-2': hasError }]"
:class="[
theme.default.input,
theme.default.spacing.horizontal,
theme.default.spacing.vertical,
theme.default.fontSize,
theme.default.borderRadius,
{ 'ring-red-500 ring-2': hasError }
]"
:style="inputStyle"
@click.prevent="showUploadModal = true"
>
<div
v-if="currentUrl == null"
class="h-6 text-gray-600 dark:text-gray-400"
class="text-gray-600 dark:text-gray-400"
>
Upload image
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 inline"
class="h-5 w-5 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -34,6 +40,7 @@
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
v-else

View File

@ -8,12 +8,12 @@
:id="id ? id : name"
:name="name"
:style="inputStyle"
class="flex items-start"
class="flex items-stretch"
>
<v-select
v-model="selectedCountryCode"
class="w-[130px]"
dropdown-class="w-[300px]"
dropdown-class="max-w-[300px]"
input-class="rounded-r-none"
:data="countries"
:disabled="disabled || countries.length === 1 ? true : null"
@ -28,13 +28,13 @@
@update:model-value="onChangeCountryCode"
>
<template #option="props">
<div class="flex items-center space-x-2 hover:text-white">
<div class="flex items-center space-x-2 max-w-full">
<country-flag
size="normal"
class="!-mt-[9px]"
class="!-mt-[9px] rounded"
:country="props.option.code"
/>
<span class="grow">{{ props.option.name }}</span>
<span class="grow truncate">{{ props.option.name }}</span>
<span>{{ props.option.dial_code }}</span>
</div>
</template>
@ -44,7 +44,7 @@
>
<country-flag
size="normal"
class="!-mt-[9px]"
class="!-mt-[9px] rounded"
:country="props.option.code"
/>
<span>{{ props.option.dial_code }}</span>
@ -58,6 +58,10 @@
:disabled="disabled ? true : null"
:class="[
theme.default.input,
theme.default.spacing.horizontal,
theme.default.spacing.vertical,
theme.default.fontSize,
theme.default.borderRadius,
{
'!ring-red-500 !ring-2': hasError,
'!cursor-not-allowed !bg-gray-200': disabled,

View File

@ -20,16 +20,10 @@
@mouseenter="onMouseHover(i)"
@mouseleave="hoverRating = -1"
>
<svg
class="w-8 h-8"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
/>
</svg>
<Icon
name="heroicons:star-20-solid"
:class="theme.RatingInput.size"
/>
</div>
</div>

View File

@ -16,6 +16,7 @@
'!cursor-not-allowed !bg-gray-200': disabled,
},
theme.RichTextAreaInput.input,
theme.RichTextAreaInput.borderRadius,
]"
:editor-toolbar="editorToolbar"
class="rich-editor resize-y"

View File

@ -11,6 +11,10 @@
:class="[
{ 'font-semibold': compVal === i },
theme.ScaleInput.button,
theme.ScaleInput.borderRadius,
theme.ScaleInput.spacing.horizontal,
theme.ScaleInput.spacing.vertical,
theme.ScaleInput.fontSize,
compVal !== i ? unselectedButtonClass : '',
]"
:style="btnStyle(i === compVal)"

View File

@ -33,7 +33,12 @@
<template #selected="{ option }">
<template v-if="multiple">
<div class="flex items-center truncate mr-6">
<span class="truncate">
<span
class="truncate"
:class="[
theme.SelectInput.fontSize,
]"
>
{{ getOptionNames(selectedValues).join(', ') }}
</span>
</div>
@ -45,7 +50,13 @@
:option-name="getOptionName(option)"
>
<div class="flex items-center truncate mr-6">
<div>{{ getOptionName(option) }}</div>
<div
:class="[
theme.SelectInput.fontSize,
]"
>
{{ getOptionName(option) }}
</div>
</div>
</slot>
</template>
@ -57,7 +68,12 @@
:selected="selected"
>
<span class="flex">
<p class="flex-grow">
<p
class="flex-grow"
:class="[
theme.SelectInput.fontSize,
]"
>
{{ option.name }}
</p>
<span
@ -85,7 +101,7 @@
</template>
<script>
import { inputProps, useFormInput } from './useFormInput.js'
import {inputProps, useFormInput} from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue'
/**
@ -93,41 +109,41 @@ import InputWrapper from './components/InputWrapper.vue'
*/
export default {
name: 'SelectInput',
components: { InputWrapper },
components: {InputWrapper},
props: {
...inputProps,
options: { type: Array, required: true },
optionKey: { type: String, default: 'value' },
emitKey: { type: String, default: 'value' },
displayKey: { type: String, default: 'name' },
loading: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
searchable: { type: Boolean, default: false },
clearable: { type: Boolean, default: false },
allowCreation: { type: Boolean, default: false },
dropdownClass: { type: String, default: 'w-full' },
remote: { type: Function, default: null }
options: {type: Array, required: true},
optionKey: {type: String, default: 'value'},
emitKey: {type: String, default: 'value'},
displayKey: {type: String, default: 'name'},
loading: {type: Boolean, default: false},
multiple: {type: Boolean, default: false},
searchable: {type: Boolean, default: false},
clearable: {type: Boolean, default: false},
allowCreation: {type: Boolean, default: false},
dropdownClass: {type: String, default: 'w-full'},
remote: {type: Function, default: null}
},
setup (props, context) {
setup(props, context) {
return {
...useFormInput(props, context)
}
},
data () {
data() {
return {
additionalOptions: [],
selectedValues: []
}
},
computed: {
finalOptions () {
finalOptions() {
return this.options.concat(this.additionalOptions)
}
},
watch: {
compVal: {
handler (newVal, oldVal) {
handler(newVal, oldVal) {
if (!oldVal) {
this.handleCompValChanged()
}
@ -135,32 +151,32 @@ export default {
immediate: false
}
},
mounted () {
mounted() {
this.handleCompValChanged()
},
methods: {
getOptionName (val) {
getOptionName(val) {
const option = this.finalOptions.find((optionCandidate) => {
return optionCandidate[this.optionKey] === val
})
if (option) return option[this.displayKey]
return null
},
getOptionNames (values) {
getOptionNames(values) {
return values.map(val => {
return this.getOptionName(val)
})
},
updateModelValue (newValues) {
updateModelValue(newValues) {
if (newValues === null) newValues = []
this.selectedValues = newValues
},
updateOptions (newItem) {
updateOptions(newItem) {
if (newItem) {
this.additionalOptions.push(newItem)
}
},
handleCompValChanged () {
handleCompValChanged() {
if (this.compVal) {
this.selectedValues = this.compVal
}

View File

@ -7,13 +7,17 @@
<VueSignaturePad
ref="signaturePad"
:class="[
theme.default.input,
theme.SignatureInput.input,
theme.SignatureInput.spacing.horizontal,
theme.SignatureInput.spacing.vertical,
theme.SignatureInput.fontSize,
theme.SignatureInput.borderRadius,
theme.SignatureInput.minHeight,
{
'!ring-red-500 !ring-2 !border-transparent': hasError,
'!cursor-not-allowed !bg-gray-200': disabled,
},
]"
height="150px"
:name="name"
:options="{ onEnd }"
/>

View File

@ -10,6 +10,10 @@
:disabled="disabled ? true : null"
:class="[
theme.default.input,
theme.default.borderRadius,
theme.default.spacing.horizontal,
theme.default.spacing.vertical,
theme.default.fontSize,
{
'!ring-red-500 !ring-2 !border-transparent': hasError,
'!cursor-not-allowed !bg-gray-200': disabled,
@ -48,18 +52,18 @@
</template>
<script>
import { inputProps, useFormInput } from "./useFormInput.js"
import {inputProps, useFormInput} from "./useFormInput.js"
import InputWrapper from "./components/InputWrapper.vue"
export default {
name: "TextAreaInput",
components: { InputWrapper },
components: {InputWrapper},
mixins: [],
props: {
...inputProps,
maxCharLimit: { type: Number, required: false, default: null },
showCharLimit: { type: Boolean, required: false, default: false },
maxCharLimit: {type: Number, required: false, default: null},
showCharLimit: {type: Boolean, required: false, default: false},
},
setup(props, context) {

View File

@ -14,6 +14,10 @@
:style="inputStyle"
:class="[
theme.default.input,
theme.default.borderRadius,
theme.default.spacing.horizontal,
theme.default.spacing.vertical,
theme.default.fontSize,
{
'!ring-red-500 !ring-2 !border-transparent': hasError,
'!cursor-not-allowed !bg-gray-200': disabled,
@ -55,23 +59,23 @@
</template>
<script>
import { inputProps, useFormInput } from "./useFormInput.js"
import {inputProps, useFormInput} from "./useFormInput.js"
import InputWrapper from "./components/InputWrapper.vue"
export default {
name: "TextInput",
components: { InputWrapper },
components: {InputWrapper},
props: {
...inputProps,
nativeType: { type: String, default: "text" },
accept: { type: String, default: null },
min: { type: Number, required: false, default: null },
max: { type: Number, required: false, default: null },
autocomplete: { type:[Boolean, String, Object], default: null },
maxCharLimit: { type: Number, required: false, default: null },
showCharLimit: { type: Boolean, required: false, default: false },
pattern: { type: String, default: null },
nativeType: {type: String, default: "text"},
accept: {type: String, default: null},
min: {type: Number, required: false, default: null},
max: {type: Number, required: false, default: null},
autocomplete: {type: [Boolean, String, Object], default: null},
maxCharLimit: {type: Number, required: false, default: null},
showCharLimit: {type: Boolean, required: false, default: false},
pattern: {type: String, default: null},
},
setup(props, context) {
@ -92,10 +96,12 @@ export default {
...useFormInput(
props,
context,
props.nativeType === "file" ? "file-" : null,
{
formPrefixKey: props.nativeType === "file" ? "file-" : null
},
),
onEnterPress,
onChange,
onChange
}
},
computed: {

View File

@ -4,25 +4,43 @@
<span />
</template>
<div class="flex">
<div class="flex space-x-2 items-center">
<v-switch
:id="id ? id : name"
v-model="compVal"
class="inline-block mr-2"
:disabled="disabled ? true : null"
:color="color"
:theme="theme"
/>
<slot name="label">
<span>{{ label }}
<span
v-if="required"
class="text-red-500 required-dot"
>*</span></span>
</slot>
<div>
<slot name="label">
<label
:aria-label="id ? id : name"
:for="id ? id : name"
:class="theme.default.fontSize"
>
{{ label }}
<span
v-if="required"
class="text-red-500 required-dot"
>*</span>
</label>
</slot>
<slot name="help">
<InputHelp
:help="help"
:help-classes="theme.default.help"
>
<template #after-help>
<slot name="bottom_after_help" />
</template>
</InputHelp>
</slot>
</div>
</div>
<template #help>
<slot name="help" />
<span class="hidden" />
</template>
<template #error>
@ -32,13 +50,15 @@
</template>
<script>
import { inputProps, useFormInput } from "./useFormInput.js"
import {inputProps, useFormInput} from "./useFormInput.js"
import VSwitch from "./components/VSwitch.vue"
import InputWrapper from "./components/InputWrapper.vue"
import InputHelp from "~/components/forms/components/InputHelp.vue"
export default {
name: "ToggleSwitchInput",
components: { InputWrapper, VSwitch },
components: {InputHelp, InputWrapper, VSwitch},
props: {
...inputProps,
},

View File

@ -0,0 +1,20 @@
<template>
<form>
<slot />
</form>
</template>
<script setup>
/**
* Used to pass props to children input components
*/
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
const props = defineProps({
themeName: { type: String, default: 'default' },
size: { type: String, default: "md" },
})
const theme = computed(() => (new ThemeBuilder(props.themeName, {size: props.size})).getAllComponents())
provide('theme', theme)
</script>

View File

@ -4,7 +4,7 @@
id="webcam"
autoplay
playsinline
:class="[{ hidden: !isCapturing }, theme.fileInput.cameraInput]"
:class="[{ hidden: !isCapturing }, theme.fileInput.minHeight, theme.fileInput.borderRadius]"
width="1280"
height="720"
/>
@ -136,7 +136,7 @@
<script>
import Webcam from "webcam-easy"
import { themes } from "~/lib/forms/form-themes.js"
import { themes } from "~/lib/forms/themes/form-themes.js"
export default {
name: "FileInput",
props: {

View File

@ -1,6 +1,6 @@
<template>
<div
:class="wrapperClass"
:class="[ twMerge(theme.default.wrapper,wrapperClass)]"
:style="inputStyle"
>
<slot name="label">
@ -54,20 +54,21 @@
<script setup>
import InputLabel from "./InputLabel.vue"
import InputHelp from "./InputHelp.vue"
import {twMerge} from "tailwind-merge"
defineProps({
id: { type: String, required: false },
name: { type: String, required: false },
label: { type: String, required: false },
form: { type: Object, required: false },
theme: { type: Object, required: true },
wrapperClass: { type: String, required: false },
inputStyle: { type: Object, required: false },
help: { type: String, required: false },
helpPosition: { type: String, default: "below_input" },
uppercaseLabels: { type: Boolean, default: true },
hideFieldName: { type: Boolean, default: true },
required: { type: Boolean, default: false },
hasValidation: { type: Boolean, default: true },
id: {type: String, required: false},
name: {type: String, required: false},
label: {type: String, required: false},
form: {type: Object, required: false},
theme: {type: Object, required: true},
wrapperClass: {type: String, required: false},
inputStyle: {type: Object, required: false},
help: {type: String, required: false},
helpPosition: {type: String, default: "below_input"},
uppercaseLabels: {type: Boolean, default: true},
hideFieldName: {type: Boolean, default: true},
required: {type: Boolean, default: false},
hasValidation: {type: Boolean, default: true},
})
</script>

View File

@ -5,8 +5,8 @@
v-model="internalValue"
:name="name"
type="checkbox"
:class="sizeClasses"
class="rounded border-gray-500 cursor-pointer checkbox"
class="rounded border-gray-500 w-10 h-10 cursor-pointer checkbox"
:class="theme.CheckboxInput.size"
:style="{ '--accent-color': color }"
:disabled="disabled ? true : null"
>
@ -32,7 +32,7 @@ const props = defineProps({
name: { type: String, default: "checkbox" },
modelValue: { type: [Boolean, String], default: false },
disabled: { type: Boolean, default: false },
sizeClasses: { type: String, default: "w-4 h-4" },
theme: { type: Object },
color: { type: String, default: null },
})

View File

@ -7,7 +7,12 @@
<div
class="inline-block w-full flex overflow-hidden"
:style="inputStyle"
:class="[theme.SelectInput.input, { '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200': disabled }, inputClass]"
:class="[
theme.SelectInput.input,
theme.SelectInput.borderRadius,
{ '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200': disabled },
inputClass
]"
>
<button
type="button"
@ -15,10 +20,18 @@
aria-expanded="true"
aria-labelledby="listbox-label"
class="cursor-pointer w-full flex-grow relative"
:class="[{'py-2': !multiple || loading, 'py-1': multiple},theme.default.inputSpacing.horizontal]"
:class="[
theme.SelectInput.spacing.horizontal,
theme.SelectInput.spacing.vertical
]"
@click="toggleDropdown"
>
<div :class="{ 'h-6': !multiple, 'min-h-8': multiple && !loading }">
<div
class="flex items-center"
:class="[
theme.SelectInput.minHeight
]"
>
<transition
name="fade"
mode="out-in"
@ -32,7 +45,6 @@
v-else-if="modelValue"
key="value"
class="flex"
:class="{ 'min-h-8': multiple }"
>
<slot
name="selected"
@ -47,7 +59,10 @@
<slot name="placeholder">
<div
class="text-gray-400 dark:text-gray-500 w-full text-left truncate pr-3"
:class="{ 'py-1': multiple && !loading }"
:class="[
{ 'py-1': multiple && !loading },
theme.SelectInput.fontSize
]"
>
{{ placeholder }}
</div>
@ -65,7 +80,7 @@
<button
v-if="clearable && !isEmpty"
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2"
:class="[theme.default.inputSpacing.vertical]"
:class="[theme.SelectInput.spacing.vertical]"
@click.prevent="clear()"
>
<Icon
@ -78,15 +93,18 @@
</div>
<collapsible
v-model="isOpen"
class="absolute mt-1 bg-white overflow-auto dark:bg-notion-dark-light shadow-xl z-10"
:class="[dropdownClass,theme.SelectInput.dropdown]"
class="absolute mt-1 bg-white overflow-auto dark:bg-notion-dark-light shadow-xl z-30"
:class="[dropdownClass,theme.SelectInput.dropdown, theme.SelectInput.borderRadius]"
@click-away="onClickAway"
>
<ul
tabindex="-1"
role="listbox"
class="text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
:class="{ 'max-h-42': !isSearchable, 'max-h-48': isSearchable }"
class="leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
:class="[
{ 'max-h-42': !isSearchable, 'max-h-48': isSearchable },
theme.SelectInput.fontSize
]"
>
<div
v-if="isSearchable"
@ -95,15 +113,29 @@
<input
v-model="searchTerm"
type="text"
class="flex-grow pl-3 pr-7 py-3 w-full focus:outline-none dark:text-white"
class="flex-grow pl-3 pr-7 py-2 w-full focus:outline-none dark:text-white"
placeholder="Search"
>
<div class="flex absolute right-0 inset-y-0 items-center px-2 justify-center pointer-events-none">
<div
v-if="!searchTerm"
class="flex absolute right-0 inset-y-0 items-center px-2 justify-center pointer-events-none"
>
<Icon
name="heroicons:magnifying-glass-solid"
class="h-5 w-5 text-gray-500 dark:text-gray-400"
/>
</div>
<div
v-else
role="button"
class="flex absolute right-0 inset-y-0 items-center px-2 justify-center"
@click="searchTerm = ''"
>
<Icon
name="heroicons:backspace"
class="h-5 w-5 text-gray-500 dark:text-gray-400"
/>
</div>
</div>
<div
v-if="loading"
@ -120,8 +152,14 @@
:key="item[optionKey]"
role="option"
:style="optionStyle"
:class="[{ 'px-3 pr-9': multiple, 'px-3': !multiple },dropdownClass,theme.SelectInput.option]"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
:class="[
dropdownClass,
theme.SelectInput.option,
theme.SelectInput.spacing.horizontal,
theme.SelectInput.spacing.vertical,
{ 'pr-9': multiple},
]"
class="text-gray-900 select-none relative cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
@click="select(item)"
>
<slot
@ -145,10 +183,11 @@
role="option"
:style="optionStyle"
:class="[{ 'px-3 pr-9': multiple, 'px-3': !multiple },dropdownClass,theme.SelectInput.option]"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
class="text-gray-900 select-none relative py-2 cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
@click="createOption(searchTerm)"
>
Create <span class="px-2 bg-gray-100 border border-gray-300 rounded group-hover-text-black">{{ searchTerm
Create <span class="px-2 bg-gray-100 border border-gray-300 rounded group-hover-text-black">{{
searchTerm
}}</span>
</li>
</div>
@ -159,38 +198,38 @@
<script>
import Collapsible from '~/components/global/transitions/Collapsible.vue'
import { themes } from '../../../lib/forms/form-themes.js'
import {themes} from '../../../lib/forms/themes/form-themes.js'
import debounce from 'debounce'
import Fuse from 'fuse.js'
export default {
name: 'VSelect',
components: { Collapsible },
components: {Collapsible},
directives: {},
props: {
data: Array,
modelValue: { default: null, type: [String, Number, Array, Object] },
inputClass: { type: String, default: null },
dropdownClass: { type: String, default: 'w-full' },
loading: { type: Boolean, default: false },
required: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
searchable: { type: Boolean, default: false },
clearable: { type: Boolean, default: false },
hasError: { type: Boolean, default: false },
remote: { type: Function, default: null },
searchKeys: { type: Array, default: () => ['name'] },
optionKey: { type: String, default: 'id' },
emitKey: { type: String, default: null },
color: { type: String, default: '#3B82F6' },
placeholder: { type: String, default: null },
uppercaseLabels: { type: Boolean, default: true },
theme: { type: Object, default: () => themes.default },
allowCreation: { type: Boolean, default: false },
disabled: { type: Boolean, default: false }
modelValue: {default: null, type: [String, Number, Array, Object]},
inputClass: {type: String, default: null},
dropdownClass: {type: String, default: 'w-full'},
loading: {type: Boolean, default: false},
required: {type: Boolean, default: false},
multiple: {type: Boolean, default: false},
searchable: {type: Boolean, default: false},
clearable: {type: Boolean, default: false},
hasError: {type: Boolean, default: false},
remote: {type: Function, default: null},
searchKeys: {type: Array, default: () => ['name']},
optionKey: {type: String, default: 'id'},
emitKey: {type: String, default: null},
color: {type: String, default: '#3B82F6'},
placeholder: {type: String, default: null},
uppercaseLabels: {type: Boolean, default: true},
theme: {type: Object, default: () => themes.default},
allowCreation: {type: Boolean, default: false},
disabled: {type: Boolean, default: false}
},
emits: ['update:modelValue', 'update-options'],
data () {
data() {
return {
isOpen: false,
searchTerm: '',
@ -198,46 +237,46 @@ export default {
}
},
computed: {
optionStyle () {
optionStyle() {
return {
'--bg-form-color': this.color
}
},
inputStyle () {
inputStyle() {
return {
'--tw-ring-color': this.color
}
},
debouncedRemote () {
debouncedRemote() {
if (this.remote) {
return debounce(this.remote, 300)
}
return null
},
filteredOptions () {
filteredOptions() {
if (!this.data) return []
if (!this.searchable || this.remote || this.searchTerm === '') {
return this.data
}
// Fuse search
const fuzeOptions = {
keys: this.searchKeys
}
const fuse = new Fuse(this.data, fuzeOptions)
return fuse.search(this.searchTerm).map((res) => {
const fuse = new Fuse(this.data, {
keys: this.searchKeys,
includeScore: true
})
return fuse.search(this.searchTerm).filter((res) => res.score < 0.5).map((res) => {
return res.item
})
},
isSearchable () {
isSearchable() {
return this.searchable || this.remote !== null || this.allowCreation
},
isEmpty () {
isEmpty() {
return this.multiple ? !this.modelValue || this.modelValue.length === 0 : !this.modelValue
}
},
watch: {
searchTerm (val) {
searchTerm(val) {
if (!this.debouncedRemote) return
if ((this.remote && val) || (val === '' && !this.modelValue) || (val === '' && this.isOpen)) {
return this.debouncedRemote(val)
@ -245,13 +284,13 @@ export default {
}
},
methods: {
onClickAway (event) {
onClickAway(event) {
// Check that event target isn't children of dropdown
if (this.$refs.select && !this.$refs.select.contains(event.target)) {
this.isOpen = false
}
},
isSelected (value) {
isSelected(value) {
if (!this.modelValue) return false
if (this.emitKey && value[this.emitKey]) {
@ -263,7 +302,7 @@ export default {
}
return this.modelValue === value
},
toggleDropdown () {
toggleDropdown() {
if (this.disabled) {
this.isOpen = false
} else {
@ -273,7 +312,7 @@ export default {
this.searchTerm = ''
}
},
select (value) {
select(value) {
if (!this.multiple) {
// Close after select
this.toggleDropdown()
@ -306,10 +345,10 @@ export default {
}
}
},
clear () {
clear() {
this.$emit('update:modelValue', this.multiple ? [] : null)
},
createOption (newOption) {
createOption(newOption) {
if (newOption) {
const newItem = {
name: newOption,

View File

@ -1,17 +1,20 @@
<template>
<div
role="button"
:id="id || name"
:aria-labelledby="id || name"
role="checkbox"
:aria-checked="props.modelValue"
class="flex"
@click.stop="onClick"
>
<div
class="inline-flex items-center h-6 w-12 p-1 bg-gray-300 border rounded-full cursor-pointer focus:outline-none transition-all transform ease-in-out duration-100"
:class="{ 'toggle-switch': props.modelValue }"
class="inline-flex items-center bg-gray-300 rounded-full cursor-pointer focus:outline-none transition-all transform ease-in-out duration-100"
:class="[{ 'toggle-switch': props.modelValue }, theme.SwitchInput.containerSize]"
:style="{ '--accent-color': props.color }"
>
<div
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-all transform ease-in-out duration-150 rounded-2xl scale-100"
:class="{ 'translate-x-5.5': props.modelValue }"
class="inline-block h-4 w-4 rounded-full bg-white transition-all transform ease-in-out duration-150 scale-100"
:class="{ [theme.SwitchInput.translatedClass]: props.modelValue}"
/>
</div>
</div>
@ -21,9 +24,12 @@
import { defineEmits, defineProps } from "vue"
const props = defineProps({
id: { type: String, default: null },
name: { type: String, default: "checkbox" },
modelValue: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
color: { type: String, default: '#3B82F6' },
theme: { type: Object },
})
const emit = defineEmits(["update:modelValue"])

View File

@ -1,29 +1,41 @@
import { ref, computed, watch } from "vue"
import { themes } from "~/lib/forms/form-themes.js"
import { default as _get } from "lodash/get"
import { default as _set } from "lodash/set"
import { default as _has } from "lodash/has"
import {ref, computed, watch} from "vue"
import {default as _get} from "lodash/get"
import {default as _set} from "lodash/set"
import {default as _has} from "lodash/has"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
export const inputProps = {
id: { type: String, default: null },
name: { type: String, required: true },
label: { type: String, required: false },
form: { type: Object, required: false },
theme: { type: Object, default: () => themes.default },
modelValue: { required: false },
required: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
placeholder: { type: String, default: null },
uppercaseLabels: { type: Boolean, default: false },
hideFieldName: { type: Boolean, default: false },
showCharLimit: { type: Boolean, default: false },
help: { type: String, default: null },
helpPosition: { type: String, default: "below_input" },
color: { type: String, default: "#3B82F6" },
wrapperClass: { type: String, default: "relative mb-3" },
id: {type: String, default: null},
name: {type: String, required: true},
label: {type: String, required: false},
form: {type: Object, required: false},
theme: {
type: Object, default: () => {
const theme = inject("theme", null)
if (theme) {
return theme.value
}
return CachedDefaultTheme.getInstance()
}
},
modelValue: {required: false},
required: {type: Boolean, default: false},
disabled: {type: Boolean, default: false},
placeholder: {type: String, default: null},
uppercaseLabels: {type: Boolean, default: false},
hideFieldName: {type: Boolean, default: false},
showCharLimit: {type: Boolean, default: false},
help: {type: String, default: null},
helpPosition: {type: String, default: "below_input"},
color: {type: String, default: "#3B82F6"},
wrapperClass: {type: String, default: ""},
}
export function useFormInput(props, context, formPrefixKey = null) {
export function useFormInput(props, context, options = {}) {
const composableOptions = {
formPrefixKey: null,
...options
}
const content = ref(props.modelValue)
const inputStyle = computed(() => {
@ -47,13 +59,13 @@ export function useFormInput(props, context, formPrefixKey = null) {
const compVal = computed({
get: () => {
if (props.form) {
return _get(props.form, (formPrefixKey || "") + props.name)
return _get(props.form, (composableOptions.formPrefixKey || "") + props.name)
}
return content.value
},
set: (val) => {
if (props.form) {
_set(props.form, (formPrefixKey || "") + props.name, val)
_set(props.form, (composableOptions.formPrefixKey || "") + props.name, val)
} else {
content.value = val
}

View File

@ -28,7 +28,7 @@
<script setup>
import { ref, defineProps, defineEmits } from "vue"
import OpenForm from "../forms/OpenForm.vue"
import { themes } from "~/lib/forms/form-themes.js"
import { themes } from "~/lib/forms/themes/form-themes.js"
const props = defineProps({
show: { type: Boolean, required: true },
form: { type: Object, required: true },

View File

@ -193,13 +193,12 @@
<script>
import OpenForm from './OpenForm.vue'
import OpenFormButton from './OpenFormButton.vue'
import { themes } from '~/lib/forms/form-themes.js'
import VButton from '~/components/global/VButton.vue'
import FormCleanings from '../../pages/forms/show/FormCleanings.vue'
import VTransition from '~/components/global/transitions/VTransition.vue'
import {pendingSubmission} from "~/composables/forms/pendingSubmission.js"
import clonedeep from "clone-deep"
import { default as _has } from 'lodash/has'
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
export default {
components: { VTransition, VButton, OpenFormButton, OpenForm, FormCleanings },
@ -227,7 +226,6 @@ export default {
return {
loading: false,
submitted: false,
themes: themes,
passwordForm: useForm({
password: null
}),
@ -241,7 +239,10 @@ export default {
return import.meta.client && window.location.href.includes('popup=true')
},
theme () {
return this.themes[_has(this.themes, this.form.theme) ? this.form.theme : 'default']
return new ThemeBuilder(this.form.theme, {
size: this.form.size,
borderRadius: this.form.border_radius
}).getAllComponents()
},
isPublicFormPage () {
return this.$route.name === 'forms-slug'

View File

@ -2,7 +2,7 @@
<button
:type="nativeType"
:disabled="loading ? true : null"
:class="`py-${sizes['p-y']} px-${sizes['p-x']} text-${sizes['font']} ${theme.Button.body}`"
:class="[`py-${sizes['p-y']} px-${sizes['p-x']} text-${sizes['font']}`,theme.Button.body,theme.Button.borderRadius]"
:style="buttonStyle"
class="btn"
>
@ -17,7 +17,7 @@
</template>
<script>
import { themes } from "~/lib/forms/form-themes.js"
import {themes} from "~/lib/forms/themes/form-themes.js"
export default {
name: "OpenFormButton",
@ -43,7 +43,7 @@ export default {
default: false,
},
theme: { type: Object, default: () => themes.default },
theme: {type: Object, default: () => themes.default},
},
computed: {

View File

@ -91,14 +91,19 @@
allow you to preview your form changes.
</div>
<form-information />
<form-structure />
<form-customization />
<form-about-submission />
<form-access />
<form-security-privacy />
<form-custom-seo />
<form-custom-code />
<VForm
size="sm"
@submit.prevent=""
>
<form-information />
<form-structure />
<form-customization />
<form-about-submission />
<form-access />
<form-security-privacy />
<form-custom-seo />
<form-custom-code />
</VForm>
</div>
<form-editor-preview />

View File

@ -55,9 +55,11 @@
<p class="flex-grow truncate">
{{ field.name }}
</p>
<v-switch
<ToggleSwitchInput
v-model="displayColumns[field.id]"
class="float-right"
wrapper-class="mb-0"
label=""
name="field.id"
@update:model-value="onChangeDisplayColumns"
/>
</div>
@ -77,9 +79,11 @@
<p class="flex-grow truncate">
{{ field.name }}
</p>
<v-switch
<ToggleSwitchInput
v-model="displayColumns[field.id]"
class="float-right"
wrapper-class="mb-0"
label=""
name="field.id"
@update:model-value="onChangeDisplayColumns"
/>
</div>
@ -98,12 +102,14 @@
class="flex flex-wrap items-end"
>
<div class="flex-grow">
<text-input
class="w-64"
:form="searchForm"
name="search"
placeholder="Search..."
/>
<VForm size="sm">
<text-input
class="w-64"
:form="searchForm"
name="search"
placeholder="Search..."
/>
</VForm>
</div>
<div class="font-semibold flex gap-4">
<p class="float-right text-xs uppercase mb-2">

View File

@ -35,8 +35,10 @@
help="Gives user a unique url to update their submission"
>
<template #label>
Editable submissions
<pro-tag class="ml-1" />
<span class="text-sm">
Editable submissions
</span>
<pro-tag class="-mt-1 ml-1" />
</template>
</toggle-switch-input>
<text-input
@ -138,7 +140,7 @@
help="Show a message, or redirect to a URL"
>
<template #selected="{ option, optionName }">
<div class="flex items-center truncate mr-6">
<div class="flex items-center text-sm truncate mr-6">
{{ optionName }}
<pro-tag
v-if="option === 'redirect'"
@ -147,8 +149,8 @@
</div>
</template>
<template #option="{ option, selected }">
<span class="flex hover:text-white">
<p class="flex-grow hover:text-white">
<span class="flex">
<p class="flex-grow">
{{ option.name }}
<template v-if="option.value === 'redirect'"><pro-tag /></template>
</p>

View File

@ -53,9 +53,34 @@
label="Form Theme"
/>
<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"
/>
<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>
<select-input
name="dark_mode"
class="mt-4"
help="To see changes, save your form and open it"
:options="[
{ name: 'Auto - use Device System Preferences', value: 'auto' },
@ -68,7 +93,6 @@
<select-input
name="width"
class="mt-4"
:options="[
{ name: 'Centered', value: 'centered' },
{ name: 'Full Width', value: 'full' },
@ -80,7 +104,6 @@
<image-input
name="cover_picture"
class="mt-4"
:form="form"
label="Cover Picture"
help="Not visible when form is embedded"
@ -89,7 +112,6 @@
<image-input
name="logo_picture"
class="mt-4"
:form="form"
label="Logo"
help="Not visible when form is embedded"
@ -98,49 +120,44 @@
<color-input
name="color"
class="mt-4"
:form="form"
label="Color (for buttons & inputs border)"
/>
<toggle-switch-input
name="hide_title"
:form="form"
class="mt-4"
label="Hide Title"
/>
<toggle-switch-input
name="no_branding"
:form="form"
class="mt-4"
>
<template #label>
Remove OpnForm Branding
<pro-tag class="ml-1" />
<span class="text-sm">
Remove OpnForm Branding
</span>
<pro-tag class="-mt-1" />
</template>
</toggle-switch-input>
<toggle-switch-input
name="show_progress_bar"
:form="form"
class="mt-4"
label="Show progress bar"
/>
<toggle-switch-input
name="uppercase_labels"
:form="form"
class="mt-4"
label="Uppercase Input Labels"
/>
<toggle-switch-input
name="transparent_background"
:form="form"
class="mt-4"
label="Transparent Background"
help="Only applies when form is embedded"
/>
<toggle-switch-input
name="confetti_on_submission"
:form="form"
class="mt-4"
label="Confetti on successful submisison"
@update:model-value="onChangeConfettiOnSubmission"
/>

View File

@ -2,17 +2,22 @@
<editor-right-sidebar
:show="form && (showEditFieldSidebar || showAddFieldSidebar)"
>
<transition mode="out-in">
<form-field-edit
v-if="showEditFieldSidebar"
:key="editFieldIndex"
v-motion-fade="'fade'"
/>
<add-form-block
v-else-if="showAddFieldSidebar"
v-motion-fade="'fade'"
/>
</transition>
<VForm
size="sm"
@submit.prevent=""
>
<transition mode="out-in">
<form-field-edit
v-if="showEditFieldSidebar"
:key="editFieldIndex"
v-motion-fade="'fade'"
/>
<add-form-block
v-else-if="showAddFieldSidebar"
v-motion-fade="'fade'"
/>
</transition>
</VForm>
</editor-right-sidebar>
</template>

View File

@ -36,7 +36,6 @@
name="tags"
label="Tags"
:form="form"
class="mt-4"
help="To organize your forms (hidden to respondents)"
placeholder="Select Tag(s)"
:multiple="true"
@ -47,7 +46,6 @@
name="visibility"
label="Visibility"
:form="form"
class="mt-4"
help="Only public form will be accessible"
placeholder="Select Visibility"
:required="true"
@ -56,7 +54,7 @@
<v-button
v-if="copyFormOptions.length > 0"
color="light-gray"
class="w-full mt-4"
class="w-full"
@click="showCopyFormSettingsModal = true"
>
<svg
@ -98,20 +96,13 @@
</svg>
</template>
<template #title>
Copy Settings from another form
Import Settings from another form
</template>
<div class="p-4 min-h-[450px]">
<p class="text-gray-600">
If you already have another form that you like to use as a base for
this form, you can do that here. Select another form, confirm, and we
will copy all of the other form settings (except the form structure)
to this form.
</p>
<div>
<select-input
v-model="copyFormId"
name="copy_form_id"
label="Copy Settings From"
class="mt-3 mb-6"
placeholder="Choose a form"
:searchable="copyFormOptions.length > 5"
:options="copyFormOptions"

View File

@ -31,14 +31,13 @@
<!-- Remember Me -->
<div class="relative flex items-center my-5">
<v-checkbox
<CheckboxInput
v-model="remember"
class="w-full md:w-1/2"
name="remember"
size="small"
>
Remember me
</v-checkbox>
label="Remember me"
/>
<div class="w-full md:w-1/2 text-right">
<a

View File

@ -109,7 +109,7 @@
<script>
import FormUrlPrefill from "../../../open/forms/components/FormUrlPrefill.vue"
import OpenForm from "../../../open/forms/OpenForm.vue"
import { themes } from "~/lib/forms/form-themes.js"
import { themes } from "~/lib/forms/themes/form-themes.js"
export default {
name: "UrlFormPrefill",

View File

@ -15,7 +15,7 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
color: "#3B82F6",
hide_title: false,
no_branding: false,
uppercase_labels: true,
uppercase_labels: false,
transparent_background: false,
closes_at: null,
closed_text:

View File

@ -1,174 +0,0 @@
/**
Input classes for each supported form themes
*/
export const themes = {
default: {
default: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full py-2 px-4 bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
inputSpacing: {
vertical: 'py-2',
horizontal: 'px-4'
},
help: 'text-gray-400 dark:text-gray-500'
},
Button: {
body: 'transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg filter hover:brightness-110'
},
DateInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'rounded-lg flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
help: 'text-gray-400 dark:text-gray-500'
},
CodeInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden',
help: 'text-gray-400 dark:text-gray-500'
},
RichTextAreaInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-1 focus:ring-opacity-100 focus:border-transparent focus:ring-2',
help: 'text-gray-400 dark:text-gray-500'
},
SelectInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'relative w-full rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 placeholder-gray-400 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-600 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent',
help: 'text-gray-400 dark:text-gray-500',
dropdown: 'rounded-lg border border-gray-300 dark:border-gray-600',
option: 'rounded-md'
},
ScaleInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
button: 'cursor-pointer text-gray-700 inline-block rounded-lg border-gray-300 px-4 py-2 flex-grow dark:bg-notion-dark-light dark:text-gray-300 text-center',
unselectedButton: 'bg-white hover:bg-gray-50 border',
help: 'text-gray-400 dark:text-gray-500'
},
SliderInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
stepLabel: 'text-gray-700 dark:text-gray-300 text-center text-xs',
help: 'text-gray-400 dark:text-gray-500'
},
fileInput: {
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded-lg',
cameraInput: 'min-h-40 rounded-lg',
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded-lg shadow-sm max-w-[10rem]'
}
},
simple: {
default: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full py-2 px-2 bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
inputSpacing: {
vertical: 'py-2',
horizontal: 'px-4'
},
help: 'text-gray-400 dark:text-gray-500'
},
Button: {
body: 'transition ease-in duration-200 text-center font-semibold focus:outline-none focus:ring-2 focus:ring-offset-2 filter hover:brightness-110'
},
DateInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 placeholder-gray-400 text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
help: 'text-gray-400 dark:text-gray-500'
},
SelectInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'relative w-full flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 placeholder-gray-400 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-600 text-base focus:outline-none focus:ring-2 focus:border-transparent',
help: 'text-gray-400 dark:text-gray-500',
dropdown: 'border border-gray-300 dark:border-gray-600',
option: ''
},
CodeInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'border border-gray-300 dark:border-gray-600 overflow-hidden',
help: 'text-gray-400 dark:text-gray-500'
},
RichTextAreaInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'border-transparent flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-1 focus:ring-opacity-100 focus:border-transparent focus:ring-2',
help: 'text-gray-400 dark:text-gray-500'
},
ScaleInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
button: 'flex-1 appearance-none border-gray-300 dark:border-gray-600 w-full py-2 px-2 bg-gray-50 text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 text-center',
unselectedButton: 'bg-white hover:bg-gray-50 border -mx-4',
help: 'text-gray-400 dark:text-gray-500'
},
SliderInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
stepLabel: 'text-gray-700 dark:text-gray-300 text-center text-xs',
help: 'text-gray-400 dark:text-gray-500'
},
fileInput: {
input: 'min-h-40 border border-dashed border-gray-300 p-4',
cameraInput: 'min-h-40',
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light shadow-sm max-w-[10rem]'
}
},
notion: {
default: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion w-full py-2 px-2 bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion',
inputSpacing: {
vertical: 'py-2',
horizontal: 'px-4'
},
help: 'text-notion-input-help dark:text-gray-500'
},
Button: {
body: 'rounded-md transition ease-in duration-200 text-center font-semibold shadow shadow-inner-notion focus:outline-none focus:ring-2 focus:ring-offset-2 filter hover:brightness-110'
},
DateInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded shadow-inner-notion border-transparent focus:border-transparent flex-1 appearance-none w-full bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion p-[1px]',
help: 'text-notion-input-help dark:text-gray-500'
},
SelectInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded relative w-full border-transparent flex-1 appearance-none bg-notion-input-background shadow-inner-notion w-full px-2 text-gray-900 placeholder-gray-400 dark:bg-notion-dark-light dark:placeholder-gray-500 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion',
help: 'text-notion-input-help dark:text-gray-500',
dropdown: 'rounded border border-gray-300 dark:border-gray-600',
option: 'rounded'
},
CodeInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded shadow-inner-notion border border-gray-300 dark:border-gray-600 overflow-hidden',
help: 'text-notion-input-help dark:text-gray-500'
},
RichTextAreaInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion border border-gray-300 dark:border-gray-600 w-full text-gray-900 bg-notion-input-background dark:bg-notion-dark-light shadow-inner dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:ring-opacity-100 focus:border-transparent focus:ring-0 focus:shadow-focus-notion',
help: 'text-notion-input-help dark:text-gray-500'
},
ScaleInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
button: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion w-full py-2 px-2 bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 text-center',
unselectedButton: 'bg-notion-input-background dark:bg-notion-dark-light hover:bg-gray-50 border',
help: 'text-notion-input-help dark:text-gray-500'
},
SliderInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
stepLabel: 'text-gray-700 dark:text-gray-300 text-center text-xs',
help: 'text-gray-400 dark:text-gray-500'
},
fileInput: {
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded bg-notion-input-background',
cameraInput: 'min-h-40 rounded',
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded shadow-sm max-w-[10rem]'
}
}
}

View File

@ -0,0 +1,21 @@
import ThemeBuilder from './ThemeBuilder.js'
const CachedDefaultTheme = (function() {
let instance
function createInstance() {
const themeBuilder = new ThemeBuilder()
return themeBuilder.getAllComponents()
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance()
}
return instance
}
}
})()
export default CachedDefaultTheme

69
client/lib/forms/themes/ThemeBuilder.js vendored Normal file
View File

@ -0,0 +1,69 @@
import {twMerge} from 'tailwind-merge'
import {themes} from './form-themes.js'
export const sizes = ['sm', 'md', 'lg']
class ThemeBuilder {
constructor(theme = 'default', options = {}) {
this.theme = themes[theme] || themes.default
this.size = options.size || 'md'
this.borderRadius = options.borderRadius || 'small'
}
extractSizedClasses(baseConfig) {
if (typeof baseConfig === 'object' &&
sizes.every((size) => baseConfig[size])) {
return baseConfig[this.size]
}
return baseConfig
}
mergeNestedClasses(baseConfig, componentConfig) {
const mergedConfig = {}
const allKeys = new Set([
...Object.keys(baseConfig),
...Object.keys(componentConfig),
])
allKeys.forEach((key) => {
const baseValue = this.extractSizedClasses(baseConfig[key])
const componentValue = this.extractSizedClasses(componentConfig[key])
if (key === 'borderRadius') {
// Special case for border radius
const borderRadiusClass = baseConfig.borderRadius?.[this.borderRadius] || ''
mergedConfig[key] = twMerge(borderRadiusClass, componentValue)
} else if (
typeof baseValue === 'object' &&
baseValue !== null &&
!Array.isArray(baseValue)) {
mergedConfig[key] = this.mergeNestedClasses(baseValue, componentValue || {})
} else {
mergedConfig[key] = twMerge(baseValue || '', componentValue || '')
}
})
return mergedConfig
}
getComponentTheme(componentName = 'default') {
const baseComponentConfig = this.theme.default || {}
const componentConfig = this.theme[componentName] || {}
return this.mergeNestedClasses(baseComponentConfig, componentConfig)
}
// Get all components classes for the selected theme
getAllComponents() {
const allComponents = {}
Object.keys(this.theme).forEach((componentName) => {
allComponents[componentName] = this.getComponentTheme(componentName)
})
return allComponents
}
}
export default ThemeBuilder

380
client/lib/forms/themes/form-themes.js vendored Normal file
View File

@ -0,0 +1,380 @@
/**
Input classes for each supported form themes
*/
export const themes = {
default: {
default: {
wrapper: {
sm: 'relative mb-2',
md: 'relative mb-3',
lg: 'relative mb-3',
},
label: 'text-gray-700 dark:text-gray-300 font-medium',
input:
'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
help: 'text-gray-500',
spacing: {
horizontal: {
sm: 'px-2',
md: 'px-4',
lg: 'px-5'
},
vertical: {
sm: 'py-1.5',
md: 'py-2',
lg: 'py-3'
}
},
fontSize: {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg'
},
borderRadius: {
none: 'rounded-none',
small: 'rounded-lg',
full: 'rounded-[20px]'
}
},
ScaleInput: {
button: 'cursor-pointer text-gray-700 inline-block border-gray-300 flex-grow dark:bg-notion-dark-light dark:text-gray-300 text-center',
unselectedButton: 'bg-white border'
},
SliderInput: {
stepLabel: 'text-gray-700 dark:text-gray-300 text-center text-xs'
},
Button: {
body: 'transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 filter hover:brightness-90'
},
CodeInput: {
input: 'overflow-hidden'
},
RichTextAreaInput: {
input:
'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-1 focus:ring-opacity-100 focus:border-transparent focus:ring-2'
},
SelectInput: {
input:
'relative w-full flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 placeholder-gray-400 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-600 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent',
dropdown: 'border border-gray-300 dark:border-gray-600',
option: 'rounded',
minHeight: {
sm: 'min-h-[20px]',
md: 'min-h-[24px]',
lg: 'min-h-[28px]'
}
},
FlatSelectInput: {
option: 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 flex items-center space-x-2 border-t first:border-t-0 px-2',
unselectedIcon: 'text-gray-300 dark:text-gray-600',
icon: {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6 mx-1'
}
},
DateInput: {
input:
'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100'
},
CheckboxInput:{
size: {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-5 h-5'
},
},
SwitchInput:{
containerSize: {
sm: 'h-5 w-10 p-0.5',
md: 'h-6 w-12 p-1',
lg: 'h-6 w-12 p-1'
},
circleSize: {
sm: 'h-4 w-4',
md: 'h-4 w-4',
lg: 'h-4 w-4'
},
translatedClass: {
sm: 'translate-x-5',
md: 'translate-x-6',
lg: 'translate-x-6'
}
},
RatingInput:{
size: {
sm: 'w-6 h-6',
md: 'w-8 h-8',
lg: 'w-10 h-10'
},
},
fileInput: {
input:
'border border-dashed border-gray-300 dark:border-gray-600 p-4 shadow-none',
minHeight: {
sm: 'min-h-28',
md: 'min-h-40',
lg: 'min-h-58'
},
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile:
'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded-lg shadow-sm max-w-[10rem]'
},
SignatureInput: {
minHeight: {
sm: 'min-h-28',
md: 'min-h-40',
lg: 'min-h-48'
},
}
},
simple: {
default: {
wrapper: {
sm: 'relative mb-2',
md: 'relative mb-3',
lg: 'relative mb-3',
},
label: 'text-gray-700 dark:text-gray-300 font-medium',
input:
'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
help: 'text-gray-500',
spacing: {
horizontal: {
sm: 'px-2',
md: 'px-4',
lg: 'px-5'
},
vertical: {
sm: 'py-1.5',
md: 'py-2',
lg: 'py-3'
}
},
fontSize: {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg'
},
borderRadius: {
none: 'rounded-none',
small: 'rounded-lg',
full: 'rounded-[20px]'
}
},
ScaleInput: {
button: 'flex-1 appearance-none border-gray-300 dark:border-gray-600 w-full bg-gray-50 text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 text-center',
unselectedButton: 'bg-white border'
},
SliderInput: {
stepLabel: 'text-gray-700 dark:text-gray-300 text-center text-xs'
},
Button: {
body: 'transition ease-in duration-200 text-center font-semibold focus:outline-none focus:ring-2 focus:ring-offset-2 filter hover:brightness-90'
},
CodeInput: {
input: 'overflow-hidden'
},
RichTextAreaInput: {
input:
'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-1 focus:ring-opacity-100 focus:border-transparent focus:ring-2'
},
SelectInput: {
input:
'relative w-full flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 placeholder-gray-400 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-600 text-base focus:outline-none focus:ring-2 focus:border-transparent',
dropdown: 'border border-gray-300 dark:border-gray-600',
option: 'rounded',
minHeight: {
sm: 'min-h-[20px]',
md: 'min-h-[24px]',
lg: 'min-h-[28px]'
}
},
FlatSelectInput: {
option: 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 flex items-center space-x-2 border-t first:border-t-0 px-2',
unselectedIcon: 'text-gray-300 dark:text-gray-600',
icon: {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6 mx-1'
}
},
DateInput: {
input: 'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 placeholder-gray-400 text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100'
},
CheckboxInput:{
size: {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-5 h-5'
},
},
SwitchInput:{
containerSize: {
sm: 'h-5 w-10',
md: 'h-6 w-12',
lg: 'h-6 w-12'
},
circleSize: {
sm: 'h-4 w-4',
md: 'h-4 w-4',
lg: 'h-4 w-4'
},
},
RatingInput:{
size: {
sm: 'w-6 h-6',
md: 'w-8 h-8',
lg: 'w-10 h-10'
},
},
fileInput: {
input:
'border border-dashed border-gray-300 dark:border-gray-600 p-4 shadow-none',
minHeight: {
sm: 'min-h-28',
md: 'min-h-40',
lg: 'min-h-48'
},
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile:
'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light shadow-sm max-w-[10rem]'
},
SignatureInput: {
minHeight: {
sm: 'min-h-28',
md: 'min-h-40',
lg: 'min-h-48'
},
}
},
notion: {
default: {
wrapper: {
sm: 'relative mb-2',
md: 'relative mb-3',
lg: 'relative mb-3',
},
label: 'text-gray-900 dark:text-gray-100 mb-1 block mt-4',
input:
'rounded border-transparent flex-1 appearance-none shadow-inner-notion w-full bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 dark:placeholder-gray-500 placeholder-gray-400 focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion',
help: 'text-gray-500',
spacing: {
horizontal: {
sm: 'px-2',
md: 'px-4',
lg: 'px-5'
},
vertical: {
sm: 'py-1.5',
md: 'py-2',
lg: 'py-3'
}
},
fontSize: {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg'
},
borderRadius: {
none: 'rounded-none',
small: 'rounded',
full: 'rounded-[20px]'
}
},
ScaleInput: {
button: 'border-transparent flex-1 appearance-none shadow-inner-notion w-full bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 text-center',
unselectedButton: 'bg-notion-input-background dark:bg-notion-dark-light border'
},
SliderInput: {
stepLabel: 'text-gray-700 dark:text-gray-300 text-center text-xs'
},
Button: {
body: 'transition ease-in duration-200 text-center font-semibold shadow shadow-inner-notion focus:outline-none focus:ring-2 focus:ring-offset-2 filter hover:brightness-90'
},
CodeInput: {
input: 'shadow-inner-notion border-transparent focus:border-transparent overflow-hidden'
},
RichTextAreaInput: {
input:
'flex-1 appearance-none shadow-inner-notion border-transparent focus:border-transparent w-full text-gray-900 bg-notion-input-background dark:bg-notion-dark-light dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:ring-opacity-100 focus:border-transparent focus:ring-0 focus:shadow-focus-notion'
},
SelectInput: {
input:
'relative w-full border-transparent flex-1 appearance-none bg-notion-input-background shadow-inner-notion w-full text-gray-900 placeholder-gray-400 dark:bg-notion-dark-light dark:placeholder-gray-500 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion',
dropdown: 'border border-gray-300 dark:border-gray-600',
option: 'rounded',
minHeight: {
sm: 'min-h-[20px]',
md: 'min-h-[24px]',
lg: 'min-h-[28px]'
}
},
FlatSelectInput: {
option: 'cursor-pointer hover:backdrop-brightness-95 flex items-center space-x-2 border-t border-neutral-300 first:border-t-0 px-2',
unselectedIcon: 'text-neutral-300 dark:text-neutral-600',
icon: {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6 mx-1'
}
},
DateInput: {
input: 'shadow-inner-notion border-transparent focus:border-transparent flex-1 appearance-none w-full bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion p-[1px]'
},
CheckboxInput:{
size: {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-5 h-5'
},
},
SwitchInput:{
containerSize: {
sm: 'h-5 w-10',
md: 'h-6 w-12',
lg: 'h-6 w-12'
},
circleSize: {
sm: 'h-4 w-4',
md: 'h-4 w-4',
lg: 'h-4 w-4'
},
},
RatingInput:{
size: {
sm: 'w-6 h-6',
md: 'w-8 h-8',
lg: 'w-10 h-10'
},
},
fileInput: {
input:
'p-4 rounded bg-notion-input-background dark:bg-notion-dark',
minHeight: {
sm: 'min-h-28',
md: 'min-h-40',
lg: 'min-h-48'
},
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile:
'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded shadow-sm max-w-[10rem]'
},
SignatureInput: {
minHeight: {
sm: 'min-h-28',
md: 'min-h-40',
lg: 'min-h-48'
},
}
}
}

View File

@ -1,4 +1,4 @@
import { themes } from "~/lib/forms/form-themes.js"
import { themes } from "~/lib/forms/themes/form-themes.js"
import { default as _has } from "lodash/has"
export default {

View File

@ -34,6 +34,7 @@
"prismjs": "^1.24.1",
"qrcode": "^1.5.1",
"query-builder-vue-3": "^1.0.1",
"tailwind-merge": "^2.3.0",
"tinymotion": "^0.2.0",
"v-calendar": "^3.1.2",
"vue": "^3.2.13",
@ -13772,11 +13773,11 @@
}
},
"node_modules/tailwind-merge": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz",
"integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz",
"integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==",
"dependencies": {
"@babel/runtime": "^7.24.0"
"@babel/runtime": "^7.24.1"
},
"funding": {
"type": "github",

View File

@ -56,6 +56,7 @@
"prismjs": "^1.24.1",
"qrcode": "^1.5.1",
"query-builder-vue-3": "^1.0.1",
"tailwind-merge": "^2.3.0",
"tinymotion": "^0.2.0",
"v-calendar": "^3.1.2",
"vue": "^3.2.13",

View File

@ -9,7 +9,8 @@ module.exports = {
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"./lib/forms/form-themes.js",
"./lib/forms/themes/form-themes.js",
"./lib/forms/themes/ThemeBuilder.js",
],
safelist: [
{

View File

@ -60,6 +60,8 @@ class FormFactory extends Factory
'description' => $this->faker->randomHtml(1),
'visibility' => 'public',
'theme' => $this->faker->randomElement(Form::THEMES),
'size' => $this->faker->randomElement(Form::SIZES),
'border_radius' => $this->faker->randomElement(Form::BORDER_RADIUS),
'width' => $this->faker->randomElement(Form::WIDTHS),
'dark_mode' => $this->faker->randomElement(Form::DARK_MODE_VALUES),
'color' => '#3B82F6',

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->string('size')->default('md');
$table->string('border_radius')->default('small');
});
// Then for each form with "Simple" theme on, disable border radius
\App\Models\Forms\Form::whereTheme('simple')->update(['border_radius' => 'none']);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->dropColumn(['size', 'border_radius']);
});
}
};