start to make inputs fancy

This commit is contained in:
MiniDigger | Martin 2022-04-03 19:21:34 +02:00
parent 0fc70b6745
commit 38b30d6724
7 changed files with 129 additions and 35 deletions

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: object | string | boolean | number | null | undefined): void; (e: "update:modelValue", value: object | string | boolean | number | null | undefined): void;
@ -19,15 +20,24 @@ const props = defineProps<{
values: Option[]; values: Option[];
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
errorMessages?: string[];
}>(); }>();
// TODO proper validation
const error = computed<boolean>(() => {
return props.errorMessages ? props.errorMessages.length > 0 : false;
});
</script> </script>
<!-- todo make fancy --> <!-- todo make fancy -->
<template> <template>
<label> <label class="relative flex" :class="{ filled: modelValue, error: error }">
<template v-if="label">{{ label }}</template> <select v-model="internalVal" :disabled="disabled" :class="inputClasses">
<select v-model="internalVal" :disabled="disabled">
<option v-for="val in values" :key="val.value" :value="val.value">{{ val.text }}</option> <option v-for="val in values" :key="val.value" :value="val.value">{{ val.text }}</option>
</select> </select>
<floating-label :label="label" />
</label> </label>
<template v-if="errorMessages && errorMessages.length > 0">
<span v-for="msg in errorMessages" :key="msg" class="text-red-500">{{ msg }}</span>
</template>
</template> </template>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
const tag = ref<string>(""); const tag = ref<string>("");
const emit = defineEmits<{ const emit = defineEmits<{
@ -12,10 +13,17 @@ const tags = computed({
const props = defineProps<{ const props = defineProps<{
modelValue: string[]; modelValue: string[];
label?: string; label?: string;
errorMessages?: string[];
counter?: boolean; counter?: boolean;
maxlength?: number; maxlength?: number;
id: string;
}>(); }>();
// TODO proper validation
const error = computed<boolean>(() => {
return props.errorMessages ? props.errorMessages.length > 0 : false;
});
if (!tags.value) tags.value = []; if (!tags.value) tags.value = [];
function remove(t: string) { function remove(t: string) {
@ -31,15 +39,19 @@ function add() {
</script> </script>
<template> <template>
<label> <div class="relative flex items-center pointer-events-none" :class="{ filled: (modelValue && modelValue.length) || tag, error: error }">
<span v-if="label">{{ label }}</span> <div :class="inputClasses">
<span v-for="t in tags" :key="t" class="bg-gray-200 rounded-4xl px-2 py-1 mx-1 inline-flex items-center" dark="text-black"> <span v-for="t in tags" :key="t" class="bg-primary-light-400 rounded-4xl px-1 py-1 mx-1 h-30px inline-flex items-center" dark="text-black">
{{ t }} {{ t }}
<button class="text-gray-400 ml-1 inline-flex" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button> <button class="text-gray-400 ml-1 inline-flex pointer-events-auto" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button>
</span> </span>
<!-- todo style the input --> <input v-model="tag" type="text" class="pointer-events-auto outline-none bg-transparent" @keydown.enter="add" />
<input v-model="tag" type="text" dark="text-black" @keydown.enter="add" /> </div>
<floating-label :label="label" />
<span v-if="counter && maxlength">{{ tags?.length || 0 }}/{{ maxlength }}</span> <span v-if="counter && maxlength">{{ tags?.length || 0 }}/{{ maxlength }}</span>
<span v-else-if="counter">{{ tags?.length || 0 }}</span> <span v-else-if="counter">{{ tags?.length || 0 }}</span>
</label> </div>
<template v-if="errorMessages && errorMessages.length > 0">
<span v-for="msg in errorMessages" :key="msg" class="text-red-500">{{ msg }}</span>
</template>
</template> </template>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value?: string): void; (e: "update:modelValue", value?: string): void;
@ -15,13 +16,17 @@ const props = defineProps<{
counter?: boolean; counter?: boolean;
maxlength?: number; maxlength?: number;
}>(); }>();
// TODO proper validation
const error = computed<boolean>(() => {
return props.errorMessages ? props.errorMessages.length > 0 : false;
});
</script> </script>
<template> <template>
<!-- todo make fancy --> <label class="relative flex" :class="{ filled: modelValue, error: error }">
<label> <input v-model="value" type="text" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" />
<template v-if="label">{{ label }}</template> <floating-label :label="label" />
<input v-model="value" type="text" :class="'w-full' + (label ? ' ml-2' : '')" v-bind="$attrs" :maxlength="maxlength" />
<span v-if="counter && maxlength">{{ value?.length || 0 }}/{{ maxlength }}</span> <span v-if="counter && maxlength">{{ value?.length || 0 }}/{{ maxlength }}</span>
<span v-else-if="counter">{{ value?.length || 0 }}</span> <span v-else-if="counter">{{ value?.length || 0 }}</span>
</label> </label>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: string): void; (e: "update:modelValue", value: string): void;
@ -11,13 +12,24 @@ const value = computed({
const props = defineProps<{ const props = defineProps<{
modelValue: string; modelValue: string;
label?: string; label?: string;
errorMessages?: string[];
counter?: boolean;
maxlength?: number;
}>(); }>();
// TODO proper validation
const error = computed<boolean>(() => {
return props.errorMessages ? props.errorMessages.length > 0 : false;
});
</script> </script>
<template> <template>
<!-- todo make fancy --> <label class="relative flex" :class="{ filled: modelValue, error: error }">
<label class="flex"> <textarea v-model="value" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" />
<span v-if="label" class="block">{{ label }}</span> <floating-label :label="label" />
<textarea v-model="value" class="ml-2 flex-grow flex-shrink" v-bind="$attrs" /> <span v-if="counter && maxlength">{{ value?.length || 0 }}/{{ maxlength }}</span>
<span v-else-if="counter">{{ value?.length || 0 }}</span>
</label> </label>
<template v-if="errorMessages && errorMessages.length > 0">
<span v-for="msg in errorMessages" :key="msg" class="text-red-500">{{ msg }}</span>
</template>
</template> </template>

View File

@ -0,0 +1,33 @@
import { defineComponent, h } from "vue";
export const inputClasses = [
"w-full outline-none p-2 border-bottom-1px rounded",
"bg-primary-light-200 border-gray-400",
"hover:(bg-primary-200/5 border-primary-400)",
"focus:(bg-primary-200/4 border-primary-300)",
"active:(bg-primary-200/5 border-primary-400)",
"error:(border-red-400)",
"disabled:(bg-black-15 text-black-50)",
"transition duration-300 ease",
];
export const labelClasses = [
"absolute origin-top-left left-2",
"input-focused:(transform scale-62) filled:(transform scale-62)",
"text-black-50 error:(!text-red-400) input-focused:(text-primary-400)",
"top-10px input-focused:(top-0) filled:(top-0)",
"transition duration-300 ease",
];
export const FloatingLabel = defineComponent({
name: "FloatingLabel",
props: {
label: {
type: String,
required: false,
},
},
render() {
return this.label ? h("p", { class: labelClasses }, this.label) : [];
},
});

View File

@ -35,9 +35,10 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
const settings = useSettingsStore(); const settings = useSettingsStore();
const visibleCategories = store.visibleCategories; const visibleCategories = store.visibleCategories;
const licenses = store.licenses;
let projectOwners!: ProjectOwner[]; // TODO move to useApi
let licenses!: string[]; const projectOwners = await useInternalApi<ProjectOwner[]>("projects/possibleOwners");
const error = ref(""); const error = ref("");
const projectCreationErrors: Ref<string[]> = ref([]); const projectCreationErrors: Ref<string[]> = ref([]);
const form: NewProjectForm = { const form: NewProjectForm = {
@ -50,7 +51,6 @@ const form: NewProjectForm = {
} as NewProjectForm; } as NewProjectForm;
const projectName = ref(form.name); const projectName = ref(form.name);
await asyncData();
form.ownerId = projectOwners[0].userId; form.ownerId = projectOwners[0].userId;
const converter = { const converter = {
@ -133,17 +133,6 @@ watch(projectName, (newName) => {
error.value = i18n.t(e.response?.data.message); error.value = i18n.t(e.response?.data.message);
}); });
}); });
async function asyncData() {
const data = await Promise.all([useInternalApi("projects/possibleOwners"), useInternalApi("data/licenses", false)]).catch((e) => {
handleRequestError(e, ctx, i18n);
});
if (typeof data === "undefined") {
return;
}
projectOwners = data[0] as ProjectOwner[];
licenses = data[1] as string[];
}
</script> </script>
<!-- todo: rules, icon --> <!-- todo: rules, icon -->

View File

@ -1,12 +1,32 @@
import { defineConfig } from "vite-plugin-windicss"; import { defineConfig } from "vite-plugin-windicss";
import colors from "windicss/colors"; import colors from "windicss/colors";
import typography from "windicss/plugin/typography"; import typography from "windicss/plugin/typography";
import plugin from "windicss/plugin";
export default defineConfig({ export default defineConfig({
darkMode: "class", darkMode: "class",
safelist: "order-last button-primary button-gray button-red", safelist: "order-last button-primary button-gray button-red",
attributify: true, attributify: true,
plugins: [typography()], plugins: [
typography(),
plugin(({ addVariant }) => {
addVariant("error", ({ style }) => {
return style.parent(".error");
});
addVariant("filled", ({ style }) => {
return style.parent(".filled");
});
addVariant("input-focused", ({ style }) => {
return style.wrapSelector((s) => "input:focus ~ " + s);
});
addVariant("input-focus-visible", ({ style }) => {
return style.wrapSelector((s) => "input:focus-visible ~ " + s);
});
addVariant("select-focused", ({ style }) => {
return style.wrapSelector((s) => "select:focus ~ " + s);
});
}),
],
theme: { theme: {
extend: { extend: {
typography: { typography: {
@ -71,6 +91,19 @@ export default defineConfig({
800: "#00102F", 800: "#00102F",
900: "#000817", 900: "#000817",
}, },
"primary-light": {
0: "#FFFFFF",
100: "#F5F8FE",
200: "#EBF1FD",
300: "#E0EAFC",
400: "#D6E3FB",
500: "#CCDCFB",
600: "#C2D4FA",
700: "#B8CDF9",
800: "#ADC6F8",
900: "#A3BFF7",
1000: "#99B8F6",
},
"background-dark-90": "#111111", "background-dark-90": "#111111",
"background-dark-80": "#181a1b", "background-dark-80": "#181a1b",
"background-light-10": "#f8faff", "background-light-10": "#f8faff",