mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 14:30:52 +01:00
mobile menu
This commit is contained in:
105
src/components/ui/button/action.vue
Normal file
105
src/components/ui/button/action.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="z-10">
|
||||
<div
|
||||
class="dropdown relative inline-flex [--placement:top] [--strategy:absolute]"
|
||||
>
|
||||
<button
|
||||
:id
|
||||
class="dropdown-toggle btn btn-primary btn-lg btn-square dropdown-open:rotate-45 transition-transform"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded="false"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<Icon
|
||||
:name="icon"
|
||||
size="46"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60 bg-transparent shadow-none"
|
||||
data-dropdown-transition
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
:aria-labelledby="id"
|
||||
>
|
||||
<li
|
||||
v-for="link in menu"
|
||||
class="dropdown-item hover:bg-transparent px-0"
|
||||
>
|
||||
<NuxtLinkLocale
|
||||
v-if="link.to"
|
||||
:to="link.to"
|
||||
class="btn btn-primary flex items-center no-underline rounded-lg flex-nowrap w-full"
|
||||
>
|
||||
<Icon
|
||||
v-if="link.icon"
|
||||
:name="link.icon"
|
||||
class="me-3"
|
||||
/>
|
||||
{{ link.label }}
|
||||
</NuxtLinkLocale>
|
||||
|
||||
<button
|
||||
v-else
|
||||
@click="link.action"
|
||||
class="link hover:link-primary flex items-center no-underline w-full"
|
||||
>
|
||||
<Icon
|
||||
v-if="link.icon"
|
||||
:name="link.icon"
|
||||
class="me-3"
|
||||
/>
|
||||
{{ link.label }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IActionMenuItem } from './types'
|
||||
|
||||
defineProps({
|
||||
menu: {
|
||||
type: Array as PropType<IActionMenuItem[]>,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'mdi:plus',
|
||||
},
|
||||
})
|
||||
|
||||
const id = useId()
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
@keyframes fadeInStagger {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. Die Listenelemente sind standardmäßig unsichtbar, damit sie nicht aufblitzen */
|
||||
.stagger-menu li {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 3. Wenn das Menü geöffnet wird, weise die Animation zu */
|
||||
:global(.dropdown-open) .stagger-menu li {
|
||||
animation-name: fadeInStagger;
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: ease-out;
|
||||
|
||||
/* SEHR WICHTIG: Sorgt dafür, dass die Elemente nach der Animation sichtbar bleiben (den Zustand von 'to' beibehalten) */
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
/* Die individuelle animation-delay wird per :style im Template gesetzt. */
|
||||
}
|
||||
</style>
|
||||
8
src/components/ui/button/types.d.ts
vendored
Normal file
8
src/components/ui/button/types.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
export interface IActionMenuItem {
|
||||
label: string
|
||||
icon?: string
|
||||
action?: () => Promise<unknown>
|
||||
to?: RouteLocationRaw
|
||||
}
|
||||
63
src/components/ui/card/index.vue
Normal file
63
src/components/ui/card/index.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<slot name="image" />
|
||||
|
||||
<div class="card-header">
|
||||
<slot name="header">
|
||||
<div
|
||||
v-if="$slots.title || title"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
v-if="icon"
|
||||
:name="icon"
|
||||
size="28"
|
||||
/>
|
||||
<h5
|
||||
v-if="title"
|
||||
class="card-title mb-0"
|
||||
>
|
||||
{{ title }}
|
||||
</h5>
|
||||
<slot
|
||||
v-else
|
||||
name="title"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-base-content/45">{{ subtitle }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="card-body px-2 sm:px-6">
|
||||
<slot />
|
||||
<div
|
||||
v-if="$slots.action"
|
||||
class="card-actions"
|
||||
>
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['close', 'submit'])
|
||||
|
||||
defineProps<{ title?: string; subtitle?: string; icon?: string }>()
|
||||
|
||||
const { escape, enter } = useMagicKeys()
|
||||
|
||||
watchEffect(async () => {
|
||||
if (escape.value) {
|
||||
await nextTick()
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(async () => {
|
||||
if (enter.value) {
|
||||
await nextTick()
|
||||
emit('submit')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -16,13 +16,13 @@
|
||||
<template #buttons>
|
||||
<slot name="buttons">
|
||||
<UiButton
|
||||
class="btn-error btn-outline"
|
||||
class="btn-error btn-outline w-full sm:w-auto"
|
||||
@click="onAbort"
|
||||
>
|
||||
<Icon name="mdi:close" /> {{ abortLabel ?? t('abort') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
class="btn-primary"
|
||||
class="btn-primary w-full sm:w-auto"
|
||||
@click="onConfirm"
|
||||
>
|
||||
<Icon name="mdi:check" /> {{ confirmLabel ?? t('confirm') }}
|
||||
|
||||
@ -17,18 +17,18 @@
|
||||
<div
|
||||
:id
|
||||
ref="modalRef"
|
||||
class="overlay modal overlay-open:opacity-100 hidden overlay-open:duration-300 modal-middle"
|
||||
class="overlay modal overlay-open:opacity-100 hidden overlay-open:duration-300 sm:modal-middle p-0 xs:p-2"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="overlay-animation-target overlay-open:mt-4 overlay-open:duration-500 mt-12 transition-all ease-out modal-dialog overlay-open:opacity-100"
|
||||
class="overlay-animation-target overlay-open:duration-300 transition-all ease-out modal-dialog overlay-open:opacity-100 pointer-events-auto overflow-y-auto"
|
||||
>
|
||||
<div class="modal-content gap-2">
|
||||
<div class="modal-content justify-between h-full max-h-none">
|
||||
<div class="modal-header">
|
||||
<div
|
||||
v-if="title || $slots.title"
|
||||
class="modal-title"
|
||||
class="modal-title py-4 break-all"
|
||||
>
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
@ -53,7 +53,7 @@
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="modal-footer flex-wrap">
|
||||
<div class="modal-footer flex-col sm:flex-row">
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,32 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<fieldset class="join w-full pt-1.5 " v-bind="$attrs">
|
||||
<fieldset
|
||||
class="join w-full"
|
||||
:class="{ 'pt-1.5': label }"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot name="prepend" />
|
||||
|
||||
<div class="input join-item">
|
||||
<Icon v-if="prependIcon" :name="prependIcon" class="my-auto shrink-0" />
|
||||
<Icon
|
||||
v-if="prependIcon"
|
||||
:name="prependIcon"
|
||||
class="my-auto shrink-0"
|
||||
/>
|
||||
|
||||
<div class="input-floating grow">
|
||||
<input
|
||||
:id ref="inputRef" v-model="input" :name="name ?? id" :placeholder="placeholder || label" :type
|
||||
:autofocus class="ps-3" :readonly="read_only" >
|
||||
<label class="input-floating-label" :for="id">{{ label }}</label>
|
||||
:id
|
||||
ref="inputRef"
|
||||
v-model="input"
|
||||
:name="name ?? id"
|
||||
:placeholder="placeholder || label"
|
||||
:type
|
||||
:autofocus
|
||||
class="ps-3"
|
||||
:readonly="read_only"
|
||||
/>
|
||||
<label
|
||||
class="input-floating-label"
|
||||
:for="id"
|
||||
>{{ label }}</label
|
||||
>
|
||||
</div>
|
||||
|
||||
<Icon v-if="appendIcon" :name="appendIcon" class="my-auto shrink-0" />
|
||||
<Icon
|
||||
v-if="appendIcon"
|
||||
:name="appendIcon"
|
||||
class="my-auto shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot name="append" class="h-auto" />
|
||||
<slot
|
||||
name="append"
|
||||
class="h-auto"
|
||||
/>
|
||||
|
||||
<UiButton
|
||||
v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
|
||||
@click="copy(`${input}`)">
|
||||
v-if="withCopyButton"
|
||||
class="btn-outline btn-accent btn-square join-item h-auto"
|
||||
@click="copy(`${input}`)"
|
||||
>
|
||||
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
|
||||
</UiButton>
|
||||
</fieldset>
|
||||
|
||||
<span v-show="errors" class="flex flex-col px-2 pt-0.5">
|
||||
<span v-for="error in errors" class="label-text-alt text-error">
|
||||
<span
|
||||
v-show="errors"
|
||||
class="flex flex-col px-2 pt-0.5"
|
||||
>
|
||||
<span
|
||||
v-for="error in errors"
|
||||
class="label-text-alt text-error"
|
||||
>
|
||||
{{ error }}
|
||||
</span>
|
||||
</span>
|
||||
@ -34,57 +69,57 @@ v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ZodSchema } from "zod";
|
||||
import type { ZodSchema } from 'zod'
|
||||
|
||||
const inputRef = useTemplateRef("inputRef");
|
||||
defineExpose({ inputRef });
|
||||
const inputRef = useTemplateRef('inputRef')
|
||||
defineExpose({ inputRef })
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<
|
||||
| "button"
|
||||
| "checkbox"
|
||||
| "color"
|
||||
| "date"
|
||||
| "datetime-local"
|
||||
| "email"
|
||||
| "file"
|
||||
| "hidden"
|
||||
| "image"
|
||||
| "month"
|
||||
| "number"
|
||||
| "password"
|
||||
| "radio"
|
||||
| "range"
|
||||
| "reset"
|
||||
| "search"
|
||||
| "submit"
|
||||
| "tel"
|
||||
| "text"
|
||||
| "time"
|
||||
| "url"
|
||||
| "week"
|
||||
| 'button'
|
||||
| 'checkbox'
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'file'
|
||||
| 'hidden'
|
||||
| 'image'
|
||||
| 'month'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'radio'
|
||||
| 'range'
|
||||
| 'reset'
|
||||
| 'search'
|
||||
| 'submit'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'time'
|
||||
| 'url'
|
||||
| 'week'
|
||||
>,
|
||||
default: "text",
|
||||
default: 'text',
|
||||
},
|
||||
label: String,
|
||||
name: String,
|
||||
prependIcon: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
prependLabel: String,
|
||||
appendIcon: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: '',
|
||||
},
|
||||
appendLabel: String,
|
||||
rules: Object as PropType<ZodSchema>,
|
||||
@ -92,44 +127,44 @@ const props = defineProps({
|
||||
withCopyButton: Boolean,
|
||||
autofocus: Boolean,
|
||||
read_only: Boolean,
|
||||
});
|
||||
})
|
||||
|
||||
const input = defineModel<string | number | undefined | null>({
|
||||
default: "",
|
||||
default: '',
|
||||
required: true,
|
||||
});
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autofocus && inputRef.value) inputRef.value.focus();
|
||||
});
|
||||
if (props.autofocus && inputRef.value) inputRef.value.focus()
|
||||
})
|
||||
|
||||
const errors = defineModel<string[] | undefined>("errors");
|
||||
const errors = defineModel<string[] | undefined>('errors')
|
||||
|
||||
const id = useId();
|
||||
const id = useId()
|
||||
|
||||
watch(input, () => checkInput());
|
||||
watch(input, () => checkInput())
|
||||
|
||||
watch(
|
||||
() => props.checkInput,
|
||||
() => {
|
||||
checkInput();
|
||||
}
|
||||
);
|
||||
checkInput()
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits(["error"]);
|
||||
const emit = defineEmits(['error'])
|
||||
|
||||
const checkInput = () => {
|
||||
if (props.rules) {
|
||||
const result = props.rules.safeParse(input.value);
|
||||
const result = props.rules.safeParse(input.value)
|
||||
//console.log('check result', result.error, props.rules);
|
||||
if (!result.success) {
|
||||
errors.value = result.error.errors.map((error) => error.message);
|
||||
emit("error", errors.value);
|
||||
errors.value = result.error.errors.map((error) => error.message)
|
||||
emit('error', errors.value)
|
||||
} else {
|
||||
errors.value = [];
|
||||
errors.value = []
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { copy, copied } = useClipboard();
|
||||
</script>
|
||||
const { copy, copied } = useClipboard()
|
||||
</script>
|
||||
|
||||
54
src/components/ui/select/color.vue
Normal file
54
src/components/ui/select/color.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-4">
|
||||
<label
|
||||
:for="id"
|
||||
class="font-medium"
|
||||
>
|
||||
{{ t('label') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
:id
|
||||
:readonly="read_only"
|
||||
:disabled="read_only"
|
||||
:title="t('title')"
|
||||
class="p-0 cursor-pointer disabled:opacity-50 disabled:pointer-events-none w-14 h-10"
|
||||
type="color"
|
||||
v-model="model"
|
||||
/>
|
||||
|
||||
<button
|
||||
@click="model = null"
|
||||
class="btn btn-sm text-sm"
|
||||
:class="{ 'btn-disabled': read_only }"
|
||||
>
|
||||
{{ t('reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const id = useId()
|
||||
const { t } = useI18n()
|
||||
|
||||
const model = defineModel()
|
||||
|
||||
defineProps({
|
||||
read_only: Boolean,
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="json">
|
||||
{
|
||||
"de": {
|
||||
"label": "Farbauswahl",
|
||||
"title": "Wähle eine Farbe aus",
|
||||
"reset": "zurücksetzen"
|
||||
},
|
||||
"en": {
|
||||
"label": "Color Picker",
|
||||
"title": "Choose a color",
|
||||
"reset": "Reset"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
37
src/components/ui/select/icon.vue
Normal file
37
src/components/ui/select/icon.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<UiSelect
|
||||
v-model="icon"
|
||||
:options="icons"
|
||||
label="Icon Picker"
|
||||
>
|
||||
<template #value="{ value }">
|
||||
<Icon
|
||||
:name="value"
|
||||
v-if="value"
|
||||
/>
|
||||
</template>
|
||||
<template #option="{ option }">
|
||||
<Icon :name="option ?? ''" />
|
||||
</template>
|
||||
</UiSelect>
|
||||
|
||||
<UiDropdown :items="icons">
|
||||
<template #activator> {{ icons.find((_icon) => _icon === icon) }}</template>
|
||||
<template #item="{ item }">
|
||||
<Icon :name="`mdi:${item}`" />
|
||||
{{ item }}
|
||||
</template>
|
||||
</UiDropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const icons = [
|
||||
'streamline:money-bank-institution-money-saving-bank-payment-finance',
|
||||
'material-symbols:star-outline-rounded',
|
||||
'pepicons-pop:smartphone-home-button',
|
||||
'majesticons:desktop-computer-line',
|
||||
'mdi:folder',
|
||||
]
|
||||
|
||||
const icon = defineModel<string | undefined | null>({ default: '' })
|
||||
</script>
|
||||
84
src/components/ui/select/index.vue
Normal file
84
src/components/ui/select/index.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div
|
||||
ref="activator"
|
||||
class="relative advance-select flex w-full input-group"
|
||||
>
|
||||
<button
|
||||
:id
|
||||
class="advance-select-toogle flex justify-between grow p-3"
|
||||
@click.prevent="toogleMenu"
|
||||
:disabled="read_only"
|
||||
>
|
||||
<slot
|
||||
name="value"
|
||||
:value
|
||||
class=""
|
||||
>
|
||||
<span>{{ value }}</span>
|
||||
</slot>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="toogleMenu"
|
||||
class="flex items-center p-2 hover:shadow rounded-md hover:bg-primary hover:text-base-content"
|
||||
:disabled="read_only"
|
||||
>
|
||||
<i class="i-[material-symbols--keyboard-arrow-down] size-4" />
|
||||
</button>
|
||||
<!-- <div data-select-dropdown="" class="absolute advance-select-menu max-h-44 top-full opened" role="listbox" tabindex="-1" aria-orientation="vertical" style="margin-top: 10px;"><div data-value="dark" data-title-value="Dunkel" tabindex="0" class="cursor-pointer advance-select-option selected:active" data-id="0"><div><div class="flex items-center"> <div class="me-2" data-icon=""><icon name="undefined" class="flex-shrink-0 size-4 text-base-content mt-1 max-w-full"></icon></div> <div class="font-semibold text-base-content" data-title="">Dunkel</div> </div> <div class="mt-1.5 text-sm text-base-content/80" data-description=""></div> </div></div><div data-value="light" data-title-value="Hell" tabindex="1" class="cursor-pointer advance-select-option selected:active" data-id="1"><div><div class="flex items-center"> <div class="me-2" data-icon=""><icon name="undefined" class="flex-shrink-0 size-4 text-base-content mt-1 max-w-full"></icon></div> <div class="font-semibold text-base-content" data-title="">Hell</div> </div> <div class="mt-1.5 text-sm text-base-content/80" data-description=""></div> </div></div><div data-value="soft" data-title-value="Soft" tabindex="2" class="cursor-pointer advance-select-option selected:active selected" data-id="2"><div><div class="flex items-center"> <div class="me-2" data-icon=""><icon name="undefined" class="flex-shrink-0 size-4 text-base-content mt-1 max-w-full"></icon></div> <div class="font-semibold text-base-content" data-title="">Soft</div> </div> <div class="mt-1.5 text-sm text-base-content/80" data-description=""></div> </div></div></div>
|
||||
class="absolute advance-select-menu max-h-44 top-full opened" -->
|
||||
<!-- Dropdown menu -->
|
||||
<ul
|
||||
data-select-dropdown
|
||||
classaaa="advance-select-menu bg-white divide-y divide-slate-100 rounded-lg shadow dark:bg-slate-700 absolute top-12"
|
||||
:class="{ hidden: !show }"
|
||||
class="absolute advance-select-menu max-h-44 top-full opened"
|
||||
role="listbox"
|
||||
tabindex="-1"
|
||||
aria-orientation="vertical"
|
||||
>
|
||||
<!-- <ul
|
||||
class=""
|
||||
:aria-labelledby="id"
|
||||
> -->
|
||||
<li
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="advance-select-option selected:active font-semibold text-base-content"
|
||||
@click=";(value = option), (show = false)"
|
||||
>
|
||||
<slot
|
||||
name="option"
|
||||
:option
|
||||
>
|
||||
{{ option }}
|
||||
</slot>
|
||||
</li>
|
||||
<!-- </ul> -->
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
|
||||
const id = useId()
|
||||
|
||||
defineProps({
|
||||
label: String,
|
||||
options: {
|
||||
type: Array as PropType<T[]>,
|
||||
default: () => [],
|
||||
},
|
||||
read_only: Boolean,
|
||||
})
|
||||
const value = defineModel<T>()
|
||||
|
||||
const show = ref(false)
|
||||
const toogleMenu = () => {
|
||||
show.value = !show.value
|
||||
}
|
||||
|
||||
const activator = ref(null)
|
||||
|
||||
onClickOutside(activator, () => (show.value = false))
|
||||
</script>
|
||||
@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<p class="bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent font-black">
|
||||
<p
|
||||
class="bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent font-black"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user