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:
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user