mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-30 14:30:08 +08:00
start to make inputs fancy
This commit is contained in:
parent
0fc70b6745
commit
38b30d6724
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
33
frontend-new/src/composables/useInputHelper.ts
Normal file
33
frontend-new/src/composables/useInputHelper.ts
Normal 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) : [];
|
||||||
|
},
|
||||||
|
});
|
@ -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 -->
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user