mobile menu

This commit is contained in:
2025-06-08 00:08:55 +02:00
parent 0f09bf8436
commit 18fee933ec
68 changed files with 4112 additions and 416 deletions

View File

@ -18,7 +18,7 @@
<li>
<NuxtLinkLocale
class="dropdown-item"
:to="{ name: 'haexSettings' }"
:to="{ name: 'settings' }"
>
<span class="icon-[tabler--settings]" />
{{ t('settings') }}

View File

@ -0,0 +1 @@
<template></template>

View File

@ -0,0 +1,33 @@
<template>
<button
aria-disabled
class="flex gap-4 w-full"
@click="$emit('click', group)"
>
<Icon
:name="groupIcon"
size="24"
class="shrink-0"
/>
<p class="w-full flex-1 text-start truncate">
{{ group.name }}
</p>
<Icon
name="mdi:chevron-right"
size="24"
/>
</button>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
defineEmits<{ click: [group: SelectHaexPasswordsGroups] }>()
const { group } = defineProps<{
group: SelectHaexPasswordsGroups
}>()
const groupIcon = computed(() => group.icon ?? 'mdi:folder-outline')
</script>

View File

@ -0,0 +1,78 @@
<template>
<ul
class="flex flex-col w-full h-full gap-y-2 *:first:rounded-t-md *:last:rounded-b-md"
ref="listRef"
>
<li
v-for="(group, index) in groupItems.groups"
class="bg-base-100 rounded-lg hover:bg-base-100/45 origin-to intersect:motion-preset-slide-down intersect:motion-ease-spring-bouncier intersect:motion-delay ease-in-out shadow"
:class="{
'bg-base-300/15 outline outline-accent hover:bg-base-300/15':
selectedItems.has(group.id),
}"
:style="{ '--motion-delay': `${50 * index}ms` }"
:key="group.id"
v-on-long-press="[
onLongPressCallbackHook,
{
delay: 1000,
},
]"
>
<HaexPassMobileMenuGroup
:group
@click="onClickGroupAsync"
class="px-4 py-2"
/>
</li>
<li
v-for="item in groupItems.items"
:key="item.id"
>
<HaexPassMobileMenuItem :item />
</li>
</ul>
</template>
<script setup lang="ts">
import type {
SelectHaexPasswordsGroups,
SelectHaexPasswordsItems,
} from '~~/src-tauri/database/schemas/vault'
import { vOnLongPress } from '@vueuse/components'
defineProps<{
groupItems: {
items: SelectHaexPasswordsItems[]
groups: SelectHaexPasswordsGroups[]
}
}>()
const selectedItems = ref<Set<string>>(new Set())
const longPressedHook = shallowRef(false)
const onLongPressCallbackHook = (_: PointerEvent) => {
longPressedHook.value = true
}
const localePath = useLocalePath()
const onClickGroupAsync = async (group: SelectHaexPasswordsGroups) => {
if (longPressedHook.value) {
if (selectedItems.value.has(group.id)) {
selectedItems.value.delete(group.id)
} else {
selectedItems.value.add(group.id)
}
if (!selectedItems.value.size) longPressedHook.value = false
} else {
await navigateTo(localePath({ name: 'passwordGroupEdit' }))
}
}
const listRef = useTemplateRef('listRef')
onClickOutside(listRef, () => {
selectedItems.value.clear()
longPressedHook.value = false
})
</script>

View File

@ -0,0 +1,13 @@
<template>
<NuxtLinkLocale>
{{ item.title }}
</NuxtLinkLocale>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsItems } from '~~/src-tauri/database/schemas/vault'
defineProps<{
item: SelectHaexPasswordsItems
}>()
</script>

View File

@ -0,0 +1,5 @@
export interface IPassMenuItem {
name: string
type: 'group' | 'item'
id: string
}

View File

@ -1,77 +1,96 @@
<template>
<aside :id ref="sidebarRef" class=" flex sm:shadow-none w-full md:max-w-64 bg-red-200" tabindex="-1">
<div class="drawer-body w-full ">
<aside
:id
ref="sidebarRef"
class="flex sm:shadow-none w-full md:max-w-64"
tabindex="-1"
>
<div class="drawer-body w-full">
<ul class="menu space-y-0.5 p-0 rounded-none md:rounded">
<li>
<a href="#">
<span class="icon-[tabler--home] size-5"/>
<span class="icon-[tabler--home] size-5" />
Home
</a>
</li>
<li class="space-y-0.5">
<a id="menu-app" class="collapse-toggle collapse-open:bg-base-content/10" data-collapse="#menu-app-collapse">
<span class="icon-[tabler--apps] size-5"/>
<a
id="menu-app"
class="collapse-toggle collapse-open:bg-base-content/10"
data-collapse="#menu-app-collapse"
>
<span class="icon-[tabler--apps] size-5" />
Apps
<span
class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4 transition-all duration-300"/>
class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4 transition-all duration-300"
/>
</a>
<ul
id="menu-app-collapse"
id="menu-app-collapse"
class="collapse hidden w-auto space-y-0.5 overflow-hidden transition-[height] duration-300"
aria-labelledby="menu-app">
aria-labelledby="menu-app"
>
<li>
<a href="#">
<span class="icon-[tabler--message] size-5"/>
<span class="icon-[tabler--message] size-5" />
Chat
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--calendar] size-5"/>
<span class="icon-[tabler--calendar] size-5" />
Calendar
</a>
</li>
<li class="space-y-0.5">
<a
id="sub-menu-academy" class="collapse-toggle collapse-open:bg-base-content/10"
data-collapse="#sub-menu-academy-collapse">
<span class="icon-[tabler--book] size-5"/>
id="sub-menu-academy"
class="collapse-toggle collapse-open:bg-base-content/10"
data-collapse="#sub-menu-academy-collapse"
>
<span class="icon-[tabler--book] size-5" />
Academy
<span class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4"/>
<span
class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4"
/>
</a>
<ul
id="sub-menu-academy-collapse"
id="sub-menu-academy-collapse"
class="collapse hidden w-auto space-y-0.5 overflow-hidden transition-[height] duration-300"
aria-labelledby="sub-menu-academy">
aria-labelledby="sub-menu-academy"
>
<li>
<a href="#">
<span class="icon-[tabler--books] size-5"/>
<span class="icon-[tabler--books] size-5" />
Courses
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--list-details] size-5"/>
<span class="icon-[tabler--list-details] size-5" />
Course details
</a>
</li>
<li class="space-y-0.5">
<a
id="sub-menu-academy-stats" class="collapse-toggle collapse-open:bg-base-content/10"
data-collapse="#sub-menu-academy-stats-collapse">
<span class="icon-[tabler--chart-bar] size-5"/>
id="sub-menu-academy-stats"
class="collapse-toggle collapse-open:bg-base-content/10"
data-collapse="#sub-menu-academy-stats-collapse"
>
<span class="icon-[tabler--chart-bar] size-5" />
Stats
<span class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4"/>
<span
class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4"
/>
</a>
<ul
id="sub-menu-academy-stats-collapse"
id="sub-menu-academy-stats-collapse"
class="collapse hidden w-auto space-y-0.5 overflow-hidden transition-[height] duration-300"
aria-labelledby="sub-menu-academy-stats">
aria-labelledby="sub-menu-academy-stats"
>
<li>
<a href="#">
<span class="icon-[tabler--chart-donut] size-5"/>
<span class="icon-[tabler--chart-donut] size-5" />
Goals
</a>
</li>
@ -83,33 +102,37 @@ id="sub-menu-academy-stats-collapse"
</li>
<li>
<a href="#">
<span class="icon-[tabler--settings] size-5"/>
<span class="icon-[tabler--settings] size-5" />
Settings
</a>
</li>
<div class="divider text-base-content/50 py-6 after:border-0">Account</div>
<div class="divider text-base-content/50 py-6 after:border-0">
Account
</div>
<li>
<a href="#">
<span class="icon-[tabler--login] size-5"/>
<span class="icon-[tabler--login] size-5" />
Sign In
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--logout-2] size-5"/>
<span class="icon-[tabler--logout-2] size-5" />
Sign Out
</a>
</li>
<div class="divider text-base-content/50 py-6 after:border-0">Miscellaneous</div>
<div class="divider text-base-content/50 py-6 after:border-0">
Miscellaneous
</div>
<li>
<a href="#">
<span class="icon-[tabler--users-group] size-5"/>
<span class="icon-[tabler--users-group] size-5" />
Support
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--files] size-5"/>
<span class="icon-[tabler--files] size-5" />
Documentation
</a>
</li>
@ -119,47 +142,47 @@ id="sub-menu-academy-stats-collapse"
</template>
<script setup lang="ts">
import type { HSOverlay } from "flyonui/flyonui";
import type { HSOverlay } from 'flyonui/flyonui'
defineProps<{ title?: string; label?: string }>();
defineProps<{ title?: string; label?: string }>()
defineEmits(["open", "close"]);
defineEmits(['open', 'close'])
const id = useId();
const id = useId()
const open = defineModel<boolean>("open", { default: true });
const open = defineModel<boolean>('open', { default: true })
const { t } = useI18n();
const { t } = useI18n()
const sidebarRef = useTemplateRef("sidebarRef");
const sidebarRef = useTemplateRef('sidebarRef')
const modal = ref<HSOverlay>();
const modal = ref<HSOverlay>()
watch(open, async () => {
if (open.value) {
await modal.value?.open();
await modal.value?.open()
} else {
await modal.value?.close(true);
await modal.value?.close(true)
}
});
})
onMounted(async () => {
if (!sidebarRef.value) return;
if (!sidebarRef.value) return
modal.value = new window.HSOverlay(sidebarRef.value, {
isClosePrev: true,
});
})
modal.value.on("close", () => {
open.value = false;
});
});
modal.value.on('close', () => {
open.value = false
})
})
</script>
<i18n lang="yaml">
de:
close: Schließen
en:
close: Close
</i18n>
de:
close: Schließen
en:
close: Close
</i18n>

View File

@ -1,11 +1,30 @@
<template>
<li
class="hover:text-primary rounded" :class="{ ['bg-base-200 text-base-content']: isActive }"
@click="triggerNavigate">
<UiTooltip :tooltip="tooltip ?? name" direction="right-start">
<NuxtLinkLocale ref="linkRef" :to class="flex items-center justify-center cursor-pointer tooltip-toogle">
<div v-if="iconType === 'svg'" class="shrink-0 size-5" v-html="icon" />
<Icon v-else :name="icon" size="1.5em" />
class="rounded"
:class="{
['bg-base-300/35 ']: isActive,
}"
@click="triggerNavigate"
>
<UiTooltip
:tooltip="tooltip ?? name"
direction="right-start"
>
<NuxtLinkLocale
ref="linkRef"
:to
class="flex items-center justify-center cursor-pointer tooltip-toogle"
>
<div
v-if="iconType === 'svg'"
class="shrink-0 size-5"
v-html="icon"
/>
<Icon
v-else
:name="icon"
size="1.5em"
/>
</NuxtLinkLocale>
</UiTooltip>
</li>
@ -26,7 +45,15 @@ const isActive = computed(() => {
props.id
)
} else {
return props.to?.name === router.currentRoute.value.meta.name
return (
props.to?.name === router.currentRoute.value.meta.name ||
router
.getRoutes()
.find((route) => route.meta.name === props.to?.name)
?.children.some(
(route) => route.meta?.name === router.currentRoute.value.meta.name,
)
)
}
})

View 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
View File

@ -0,0 +1,8 @@
import type { RouteLocationRaw } from 'vue-router'
export interface IActionMenuItem {
label: string
icon?: string
action?: () => Promise<unknown>
to?: RouteLocationRaw
}

View 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>

View File

@ -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') }}

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -2,7 +2,7 @@
<UiDialog
v-model:open="open"
:title="t('title')"
class="btn btn-primary btn-outline shadow-md md:btn-lg"
class="btn btn-primary btn-outline shadow-md btn-lg"
@click="open = true"
>
<template #trigger>
@ -34,14 +34,14 @@
<template #buttons>
<UiButton
class="btn-error"
class="btn-error w-full sm:w-auto"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton
class="btn-primary"
class="btn-primary w-full sm:w-auto"
@click="onCreateAsync"
>
{{ t('create') }}

View File

@ -1,7 +1,7 @@
<template>
<UiDialogConfirm
v-model:open="open"
class="btn btn-primary btn-outline shadow-md md:btn-lg"
class="btn btn-primary btn-outline shadow-md btn-lg"
:confirm-label="t('open')"
:abort-label="t('abort')"
@abort="onAbort"
@ -12,13 +12,14 @@
<i18n-t
keypath="title"
tag="p"
class="flex gap-2"
class="flex gap-x-2 flex-wrap"
>
<template #haexvault>
<UiTextGradient>HaexVault</UiTextGradient>
</template>
</i18n-t>
<p class="text-sm">{{ database.path }}</p>
<div class="text-sm">{{ props.path ?? database.path }}</div>
</template>
<template #trigger>
@ -63,11 +64,6 @@ const database = reactive<{
type: 'password',
})
watch(
() => props.path,
() => (database.path = props.path),
)
const initDatabase = () => {
database.name = ''
database.password = ''

View File

@ -65,7 +65,7 @@
</div>
<div
class="flex flex-col items-center w-full min-h-14 gap-2 py-1"
class="flex items-center w-full min-h-14 gap-2 py-1"
:style="{ color }"
>
<Icon
@ -78,7 +78,7 @@
v-show="read_only"
class="overflow-hidden whitespace-nowrap"
>
{{ title }}
a{{ title }}
</h5>
</div>
</div>
@ -91,6 +91,7 @@
class="fixed bottom-2 left-0 w-full flex items-center justify-between px-4 md:hidden"
>
<div class="transition-all duration-500">
aa
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"

View File

@ -3,59 +3,61 @@
<slot name="image" />
<div class="card-header">
<div v-if="$slots.title || title">
<Icon v-if="icon" :name="icon" />
<h5 v-if="title" class="card-title mb-0">
{{ title }}
</h5>
<slot v-else name="title" />
</div>
<div class="text-base-content/50">Your journey starts here</div>
<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/50">{{ subtitle }}</div>
</slot>
</div>
<div class="card-body">
<div class="card-body px-2 sm:px-6">
<slot />
aaaaaaaaa
<div v-if="$slots.action" class="card-actions">
<div
v-if="$slots.action"
class="card-actions"
>
<slot name="action" />
</div>
</div>
</div>
<!-- <div class="bg-base-100 w-full mx-auto shadow h-full overflow-hidden pt-[7.5rem]">
<div
class="fixed top-0 right-0 z-10 transition-all duration-700 w-full font-semibold text-lg h-[7.5rem]"
>
<div
class="justify-center items-center flex flex-wrap border-b rounded-b border-secondary h-full"
>
<slot name="header" />
</div>
</div>
<div class="h-full overflow-scroll bg-base-200">
<slot />
</div>
</div> -->
</template>
<script setup lang="ts">
const emit = defineEmits(["close", "submit"]);
const emit = defineEmits(['close', 'submit'])
defineProps<{ title?: string; icon?: string }>();
defineProps<{ title?: string; subtitle?: string; icon?: string }>()
const { escape, enter } = useMagicKeys();
const { escape, enter } = useMagicKeys()
watchEffect(async () => {
if (escape.value) {
await nextTick();
emit("close");
await nextTick()
emit('close')
}
});
})
watchEffect(async () => {
if (enter.value) {
await nextTick();
emit("submit");
await nextTick()
emit('submit')
}
});
})
</script>

View File

@ -17,7 +17,6 @@
v-model.trim="vaultGroup.name"
:label="t('vaultGroup.name')"
:placeholder="t('vaultGroup.name')"
:rules="vaultGroupSchema.name"
:with-copy-button="read_only"
:read_only
autofocus
@ -29,18 +28,17 @@
:read_only
:label="t('vaultGroup.description')"
:placeholder="t('vaultGroup.description')"
:rules="vaultGroupSchema.description"
:with-copy-button="read_only"
/>
<UiColorPicker
<UiSelectColor
v-model="vaultGroup.color"
:read_only
:label="t('vaultGroup.color')"
:placeholder="t('vaultGroup.color')"
/>
<UiIconPicker
<UiSelectIcon
v-model="vaultGroup.icon"
:read_only
:label="t('vaultGroup.icon')"
@ -51,29 +49,26 @@
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
import {
vaultGroupSchema,
type SelectVaultGroup,
} from '~/database/schemas/vault';
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
const { t } = useI18n();
const showConfirmation = ref(false);
const vaultGroup = defineModel<SelectVaultGroup>({ required: true });
const read_only = defineModel<boolean>('read_only');
const { t } = useI18n()
const showConfirmation = ref(false)
const vaultGroup = defineModel<SelectHaexPasswordsGroups>({ required: true })
const read_only = defineModel<boolean>('read_only')
const props = defineProps({
originally: Object as PropType<SelectVaultGroup>,
});
originally: Object as PropType<SelectHaexPasswordsGroups>,
})
defineEmits<{
submit: [to?: RouteLocationNormalizedLoadedGeneric];
close: [void];
back: [void];
reject: [to?: RouteLocationNormalizedLoadedGeneric];
}>();
submit: [to?: RouteLocationNormalizedLoadedGeneric]
close: [void]
back: [void]
reject: [to?: RouteLocationNormalizedLoadedGeneric]
}>()
const hasChanges = computed(() => {
console.log('group has changes', props.originally, vaultGroup.value);
console.log('group has changes', props.originally, vaultGroup.value)
if (!props.originally) {
if (
vaultGroup.value.color?.length ||
@ -81,13 +76,13 @@ const hasChanges = computed(() => {
vaultGroup.value.icon?.length ||
vaultGroup.value.name?.length
) {
return true;
return true
} else {
return false;
return false
}
}
return JSON.stringify(props.originally) !== JSON.stringify(vaultGroup.value);
});
return JSON.stringify(props.originally) !== JSON.stringify(vaultGroup.value)
})
/* const onClose = () => {
if (props.originally) vaultGroup.value = { ...props.originally };