back to flyonui

This commit is contained in:
2025-05-22 23:00:25 +02:00
parent 96fa1fb0e4
commit ffc2184806
31 changed files with 1140 additions and 550 deletions

View File

@ -1,16 +1,15 @@
<template>
<div class=" ">
<BaseDialog />
<!-- <NuxtLayout>
<div>
<NuxtLayout :data-theme="currentTheme.value">
<NuxtPage />
<Toaster />
</NuxtLayout> -->
<NuxtSnackbar />
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
//import 'vue-sonner/style.css'
//const { currentTheme } = storeToRefs(useUiStore())
const { currentTheme } = storeToRefs(useUiStore())
</script>
<style>

View File

@ -1,228 +1,9 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@import 'tailwindcss';
@import 'flyonui/variants.css';
@plugin "@iconify/tailwind4";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.129 0.042 264.695);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.129 0.042 264.695);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.984 0.003 247.858);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.279 0.041 260.031);
--input: oklch(0.279 0.041 260.031);
--ring: oklch(0.446 0.043 257.281);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(0.279 0.041 260.031);
--sidebar-ring: oklch(0.446 0.043 257.281);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
@plugin "flyonui" {
themes: all;
}
@source "../../node_modules/flyonui/flyonui.js";

View File

@ -0,0 +1,14 @@
<template>
<button class="btn join-item" :type>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps({
type: {
type: String as PropType<"reset" | "submit" | "button">,
default: "button",
},
});
</script>

View File

@ -0,0 +1,15 @@
<template>
<button :class="cn(
`relative flex items-center justify-center min-w-28 min-h-10 overflow-hidden outline-2 outline-offset-2 rounded cursor-pointer`, className
)
">
<span class="btn-content inline-flex size-full items-center justify-center px-4 py-2 gap-2">
<slot />
</span>
</button>
</template>
<script setup lang="ts">
import { cn } from "@/lib/utils";
const { className = "primary" } = defineProps<{ className?: string }>()
</script>

View File

@ -0,0 +1,102 @@
<template>
<div>
<fieldset class="join w-full">
<slot name="prepend" />
<!-- <div class="">
-->
<HaexButton 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'" />
</Haexbutton>
<!-- <div class="">
<input :id :name="name ?? id" :placeholder="placeholder || label" :type :autofocus class="" v-bind="$attrs"
v-model="input" ref="inputRef" :readonly="read_only" />
<label class="floating-label" :for="id">{{ label }}</label>
</div> -->
<label class="floating-label input join-item">
<Icon v-if="iconPrepend" :name="iconPrepend" class="my-auto size-6" />
<span>Your Email</span>
<input type="text" placeholder="mail@site.com" class=" join-item " />
<Icon v-if="iconAppend" :name="iconAppend" class="my-auto shrink-0" />
</label>
<!-- <Icon v-if="iconAppend" :name="iconAppend" class="my-auto shrink-0" />
</div> -->
<slot name="append" class="h-auto" />
<HaexButton 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'" />
</Haexbutton>
</fieldset>
<span class="flex flex-col px-2 pt-0.5" v-show="errors">
<span v-for="error in errors" class="label-text-alt text-error">
{{ error }}
</span>
</span>
</div>
<!-- <div class="relative w-full max-w-sm items-center">
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
<Icon v-if="iconPrepend" :name="iconPrepend" class="size-6" />
<button>aa</button>
</span>
<Input id="search" type="text" placeholder="Search..." :class="{ 'pl-10': iconPrepend, 'pr-10': iconAppend }" />
<span class="absolute end-0 inset-y-0 flex items-center justify-center px-2">
<Icon v-if="iconAppend" :name="iconAppend" class="size-6" />
</span>
</div> -->
</template>
<script setup lang="ts">
import { HaexButton } from '#components';
import type { ZodSchema } from 'zod';
const id = useId()
const props = defineProps<{ iconAppend?: string, iconPrepend?: string, placeholder?: string, type?: string, label?: string, withCopyButton?: boolean, rules?: ZodSchema, read_only?: boolean, autofocus?: boolean, checkInput?: boolean, name?: string }>()
const inputRef = useTemplateRef('inputRef')
const input = defineModel<string | number | undefined | null>({
default: '',
required: true,
})
onMounted(() => {
if (props.autofocus && inputRef.value) inputRef.value.focus()
})
watch(input, () => checkInput())
watch(
() => props.checkInput,
() => {
checkInput()
}
)
const emit = defineEmits(['error'])
const errors = defineModel<string[] | undefined>('errors')
const checkInput = () => {
if (props.rules) {
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)
} else {
errors.value = []
}
}
}
const { copy, copied } = useClipboard()
</script>

View File

@ -1,41 +1,36 @@
<template>
<UiDropdown
activator-class="btn btn-text btn-circle "
dropdown-class="[--offset:20]"
>
<UiDropdown activator-class="btn btn-text btn-circle " dropdown-class="[--offset:20]">
<template #activator>
<div
class="size-9.5 rounded-full items-center justify-center text-base-content text-base"
>
<div class="size-9.5 rounded-full items-center justify-center text-base-content text-base">
<Icon name="mdi:format-list-bulleted" class="size-full p-2" />
</div>
</template>
<template #items>
<ul
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-avatar"
>
<li>
<NuxtLinkLocale class="dropdown-item" :to="{ name: 'haexSettings' }">
<span class="icon-[tabler--settings]"></span>
{{ t('settings') }}
</NuxtLinkLocale>
</li>
<li class="dropdown-footer gap-2">
<button
class="btn btn-error btn-soft btn-block"
@click="onVaultCloseAsync"
>
<span class="icon-[tabler--logout]"></span>
{{ t('vault.close') }}
</button>
</li>
</ul>
<!-- <ul class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60" role="menu" aria-orientation="vertical"
aria-labelledby="dropdown-avatar"> -->
<template #items>
<li>
<NuxtLinkLocale class="dropdown-item" :to="{ name: 'haexSettings' }">
<span class="icon-[tabler--settings]"></span>
{{ t('settings') }}
</NuxtLinkLocale>
</li>
<li class="dropdown-footer gap-2">
<button class="btn btn-error btn-soft btn-block" @click="onVaultCloseAsync">
<span class="icon-[tabler--logout]"></span>
{{ t('vault.close') }}
</button>
</li>
</template>
<!--
</ul> -->
</UiDropdown>
</template>

View File

@ -0,0 +1,14 @@
<template>
<button class="btn join-item" :type>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps({
type: {
type: String as PropType<"reset" | "submit" | "button">,
default: "button",
},
});
</script>

View File

@ -0,0 +1,97 @@
<template>
<button v-bind="$attrs" type="button" aria-haspopup="dialog" aria-expanded="false" :aria-label="label"
class="--prevent-on-load-init " @click="$emit('open')">
<slot name="trigger">open</slot>
</button>
<div :id class="overlay modal overlay-open:opacity-100 hidden overlay-open:duration-300" role="dialog" ref="modalRef"
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">
<div class="modal-content">
<div class="modal-header">
<slot name="title">
<h3 v-if="title" class="modal-title text-base sm:text-lg">
{{ title }}
</h3>
</slot>
<button type="button" class="btn btn-text btn-circle btn-sm absolute end-3 top-3" :aria-label="t('close')"
tabindex="1" @click="open = false">
<Icon name="mdi:close" size="18" />
</button>
</div>
<div class="modal-body text-sm sm:text-base py-1">
<slot />
</div>
<div class="modal-footer flex-wrap">
<slot name="buttons" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { HSOverlay } from "flyonui/flyonui";
defineOptions({
inheritAttrs: false,
});
defineProps<{ title?: string; label?: string }>();
defineEmits(["open", "close"]);
const id = useId();
const open = defineModel<boolean>("open", { default: false });
const { t } = useI18n();
const modalRef = useTemplateRef("modalRef");
defineExpose({ modalRef });
const modal = ref<HSOverlay>();
watch(open, async () => {
console.log("watch open modal", open.value, modal.value);
if (open.value) {
await modal.value?.open();
} else {
await modal.value?.close(true);
//HSOverlay.close(`#${id}`);
//console.log("close dialog");
}
});
onMounted(async () => {
if (!modalRef.value) return;
// flyonui has a problem importing HSOverlay at component level due to ssr
// that's the workaround I found
//const flyonui = await import("flyonui/flyonui");
modal.value = new window.HSOverlay(modalRef.value, {
isClosePrev: true,
});
modal.value.on("close", () => {
console.log("on close", open.value);
open.value = false;
});
/* modal.value.on("open", () => {
console.log("on open", open.value);
open.value = true;
}); */
});
</script>
<i18n lang="json">{
"de": {
"close": "Schließen"
},
"en": {
"close": "Close"
}
}</i18n>

View File

@ -0,0 +1,44 @@
<template>
<div class="dropdown relative inline-flex">
<button :id class="dropdown-toggle " v-bind="$attrs" aria-haspopup="menu" aria-expanded="false" :aria-label="label">
<slot name="activator">
{{ label }}
<span class="icon-[tabler--chevron-down] dropdown-open:rotate-180 size-4">
</span>
</slot>
</button>
<ul class="dropdown-menu dropdown-open:opacity-100 hidden min-w-28" role="menu" aria-orientation="vertical"
:aria-labelledby="id">
<slot name="items">
<li v-for="item in items" :is="itemIs" class="dropdown-item" @click="$emit('select', item)">
<slot name="item" :item>
{{ item }}
</slot>
</li>
</slot>
</ul>
</div>
</template>
<script setup lang="ts" generic="T">
const { itemIs = "li" } = defineProps<{
label?: string;
items?: T[];
itemIs?: string;
activatorClass?: string;
}>();
defineOptions({
inheritAttrs: false,
});
defineEmits<{ select: [T] }>();
const id = useId();
</script>

View File

@ -0,0 +1,33 @@
<template>
<UiDropdown :items="availableLocales" @select="(locale) => $emit('select', locale)"
class="btn btn-primary btn-outline">
<template #activator>
<Icon :name="flags[locale]" />
<Icon name="tabler:chevron-down" class="dropdown-open:rotate-180 size-4" />
</template>
<template #item="{ item }">
<div class="flex gap-2 justify-center">
<Icon :name="flags[item]" class="my-auto" />
<p>
{{ item }}
</p>
</div>
</template>
</UiDropdown>
</template>
<script setup lang="ts">
import { type Locale } from 'vue-i18n'
const flags = {
de: 'emojione:flag-for-germany',
en: 'emojione:flag-for-united-kingdom',
}
const { availableLocales, locale } = useI18n()
defineEmits<{ select: [Locale] }>()
</script>

View File

@ -0,0 +1,22 @@
<template>
<UiDropdown :items="availableThemes" @select="(theme) => $emit('select', theme)" class="btn btn-primary btn-outline">
<template #activator>
<Icon :name="currentTheme.icon" />
</template>
<template #item="{ item }">
<div class="flex gap-2 justify-center">
<Icon :name="item.icon" class="my-auto" />
<p>
{{ item.name }}
</p>
</div>
</template>
</UiDropdown>
</template>
<script setup lang="ts">
const { availableThemes, currentTheme } = storeToRefs(useUiStore())
defineEmits<{ select: [ITheme] }>()
</script>

View File

@ -0,0 +1,133 @@
<template>
<div>
<fieldset class="join w-full pt-0.5">
<slot name="prepend" />
<div class="input join-item">
<Icon v-if="prependIcon" :name="prependIcon" class="my-auto shrink-0" />
<div class="input-floating grow">
<input :id :name="name ?? id" :placeholder="placeholder || label" :type :autofocus class="ps-3"
v-bind="$attrs" v-model="input" ref="inputRef" :readonly="read_only" />
<label class="input-floating-label" :for="id">{{ label }}</label>
</div>
<Icon v-if="appendIcon" :name="appendIcon" class="my-auto shrink-0" />
</div>
<slot name="append" class="h-auto" />
<UiButton 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 class="flex flex-col px-2 pt-0.5" v-show="errors">
<span v-for="error in errors" class="label-text-alt text-error">
{{ error }}
</span>
</span>
</div>
</template>
<script setup lang="ts">
import { type ZodSchema } from "zod";
const inputRef = useTemplateRef("inputRef");
defineExpose({ inputRef });
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
placeholder: {
type: String,
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"
>,
default: "text",
},
label: String,
name: String,
prependIcon: {
type: String,
default: "",
},
prependLabel: String,
appendIcon: {
type: String,
default: "",
},
appendLabel: String,
rules: Object as PropType<ZodSchema>,
checkInput: Boolean,
withCopyButton: Boolean,
autofocus: Boolean,
read_only: Boolean,
});
const input = defineModel<string | number | undefined | null>({
default: "",
required: true,
});
onMounted(() => {
if (props.autofocus && inputRef.value) inputRef.value.focus();
});
const errors = defineModel<string[] | undefined>("errors");
const id = useId();
watch(input, () => checkInput());
watch(
() => props.checkInput,
() => {
checkInput();
}
);
const emit = defineEmits(["error"]);
const checkInput = () => {
if (props.rules) {
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);
} else {
errors.value = [];
}
}
};
const { copy, copied } = useClipboard();
</script>

View File

@ -0,0 +1,42 @@
<template>
<UiInput :check-input :label="label || t('password')" :placeholder="placeholder || t('password')" :rules :type="type"
:autofocus v-model="value">
<template #append>
<UiButton class="btn-outline btn-accent btn-square h-auto" @click="tooglePasswordType">
<Icon :name="type === 'password' ? 'mdi:eye-off' : 'mdi:eye'" />
</UiButton>
</template>
</UiInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from "zod";
const { t } = useI18n();
const value = defineModel<string | number | null | undefined>();
defineProps({
label: String,
placeholder: String,
checkInput: Boolean,
rules: Object as PropType<ZodSchema>,
autofocus: Boolean,
});
const type = ref<"password" | "text">("password");
const tooglePasswordType = () => {
type.value = type.value === "password" ? "text" : "password";
};
</script>
<i18n lang="json">{
"de": {
"password": "Passwort"
},
"en": {
"password": "Password"
}
}</i18n>

View File

@ -0,0 +1,53 @@
<template>
<svg id="logo" class="fill-current stroke-current w-[160px]" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 286.3 85" xml:space="preserve">
<switch>
<g>
<g class="logo-imagesss">
<circle fill="white" cx="42.5" cy="42.5" r="40"></circle>
<path d="M42.3,83.4c-22.6,0-40.9-18.4-40.9-40.9c0-22.6,18.4-40.9,40.9-40.9c22.6,0,40.9,18.4,40.9,40.9
C83.3,65.1,64.9,83.4,42.3,83.4z M42.3,5.8C22.1,5.8,5.7,22.3,5.7,42.5s16.5,36.7,36.7,36.7S79,62.7,79,42.5S62.6,5.8,42.3,5.8z
"></path>
<g>
<g>
<polygon points="38.8,69.8 38.8,31.7 22.3,31.7 22.3,38.5 29.8,38.5 29.8,69.8 "></polygon>
<path d="M34.1,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6
C39.9,15.9,37.3,13.2,34.1,13.2z"></path>
</g>
<g>
<polygon points="45.9,69.8 45.9,31.7 62.4,31.7 62.4,38.5 54.9,38.5 54.9,69.8 "></polygon>
<path d="M50.6,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C44.8,15.9,47.4,13.2,50.6,13.2z"></path>
</g>
</g>
</g>
<g class="logo-textsss">
<path
d="M136.1,63.6c-4,0-5.3-2.6-5.3-6V38.5h10.6v-6.7h-10.6v-6.7h-9c0,7,0,29.1,0,32.7
c0,4.2,1.6,7.5,3.8,9.7c2.3,2.2,5.6,3.3,9.8,3.3c5.1,0,8.4-1.8,10.6-4.2l-4.7-6C140.2,62.1,138.5,63.6,136.1,63.6z">
</path>
<path d="M217.7,30.7c-4.9,0-8.2,1.6-10.4,3.8c-2.2-2.2-5.5-3.8-10.4-3.8c-15,0-14.9,12.1-14.9,15
s0,24.1,0,24.1h9V45.7c0-8.5,4.9-8.3,5.9-8.3c1,0,5.9-0.3,5.9,8.3v24.1h0h9h0V45.7c0-8.5,4.9-8.3,5.9-8.3c1,0,5.9-0.3,5.9,8.3
v24.1h9c0,0,0-21.2,0-24.1C232.6,42.8,232.7,30.7,217.7,30.7z"></path>
<path d="M273.2,46.4c-4.3-1.4-6-2.5-6-5.2c0-2,1.1-3.8,4.3-3.8c3.2,0,4.5,3.3,5.1,4.8
c2.7-1.5,5.3-2.9,6.6-3.6c-2.5-6-6.3-7.9-12-7.9c-8,0-11.7,5.5-11.7,10.6c0,6.5,2.9,9.8,11.2,12.2c6,1.8,6.5,4.7,6.2,6.2
c-0.3,1.7-1.6,3.6-5.3,3.6c-3.6,0-5.8-3.8-6.8-5.4c-1.8,1.1-3.4,2.1-6.4,3.8c2.1,5,6.8,9.1,13.5,9.1c7.9,0,12.9-5.1,12.9-12.1
C284.9,51,279.6,48.5,273.2,46.4z"></path>
<g>
<polygon points="239.7,69.8 239.7,31.7 256.2,31.7 256.2,38.5 248.7,38.5 248.7,69.8 "></polygon>
<path d="M244.4,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C238.6,15.9,241.2,13.2,244.4,13.2z"></path>
</g>
<g>
<polygon points="114.7,69.8 114.7,31.7 98.1,31.7 98.1,38.5 105.7,38.5 105.7,69.8 "></polygon>
<path d="M110,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6C115.8,15.9,113.2,13.2,110,13.2
z"></path>
</g>
<path d="M176.4,52.4v-3.7c0-12.3-4.7-18-14.8-18c-9.3,0-14.7,6.6-14.7,18v4c0,11.5,5.8,18.2,15.8,18.2
c6.6,0,10.8-3.7,12.7-7.9c-2.2-1.4-4.6-2.8-6.1-3.8c-1,1.7-2.9,4.4-6.7,4.4c-5.8,0-7-5.9-7-10.9v-0.2H176.4z M155.7,45.7
c0.2-7.1,3.3-8.2,6-8.2c2.6,0,5.9,1,6,8.2H155.7z"></path>
</g>
</g>
</switch>
</svg>
</template>

View File

@ -0,0 +1,5 @@
<template>
<p class="bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent font-black">
<slot />
</p>
</template>

View File

@ -0,0 +1,52 @@
<template>
<div class="tooltip [--prevent-popper:false]">
<div class="tooltip-toggle" aria-label="Tooltip">
<slot>
<button class="btn btn-square">
<Icon name="mdi:chevron-up-box-outline" />
</button>
</slot>
<span class="tooltip-content tooltip-shown:opacity-100 tooltip-shown:visible z-40" role="tooltip">
<span class="tooltip-body" v-bind="$attrs">
{{ tooltip }}
</span>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue';
const props = defineProps({
direction: {
type: String as PropType<
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'right'
| 'right-start'
| 'right-end'
| 'left'
| 'left-start'
| 'left-end'
>,
default: 'top',
},
tooltip: String,
trigger: {
type: String as PropType<'focus' | 'hover' | 'click'>,
default: 'hover',
},
});
defineOptions({
inheritAttrs: false,
});
</script>

View File

@ -1,41 +1,32 @@
<template>
<Dialog>
<!-- class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap" -->
<DialogTrigger as-child>
<Button>
<UiDialog :title="t('title')" v-model:open="open"
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap">
<template #trigger>
<Icon name="mdi:plus" />
{{ t('database.create') }}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<Icon name="mdi:plus" />
{{ t('database.create') }}
<form class="flex flex-col gap-4" @submit="onCreateAsync">
<Input :check-input="check" :label="t('database.label')" :placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name" autofocus prepend-icon="mdi:safe" v-model="database.name" />
</template>
<!-- <UiInputPassword :check-input="check" :rules="vaultDatabaseSchema.password" prepend-icon="mdi:key-outline"
v-model="database.password" /> -->
</form>
<form class="flex flex-col gap-4" @submit="onCreateAsync">
<UiInput :check-input="check" :label="t('database.label')" :placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name" autofocus prepend-icon="mdi:safe" v-model="database.name" />
<DialogFooter>
<Button class="btn-error" @click="onClose">
{{ t('abort') }}
</Button>
<UiInputPassword :check-input="check" :rules="vaultDatabaseSchema.password" prepend-icon="mdi:key-outline"
v-model="database.password" />
</form>
<Button class="btn-primary" @click="onCreateAsync">
{{ t('create') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<template #buttons>
<UiButton class="btn-error" @click="onClose">
{{ t('abort') }}
</UiButton>
<UiButton class="btn-primary" @click="onCreateAsync">
{{ t('create') }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
@ -43,7 +34,6 @@ import { save } from '@tauri-apps/plugin-dialog'
import { onKeyStroke } from '@vueuse/core'
import { useVaultStore } from '~/stores/vault'
import { vaultDatabaseSchema } from './schema'
import { toast } from 'vue-sonner'
onKeyStroke('Enter', (e) => {
e.preventDefault()
@ -76,7 +66,7 @@ const initDatabase = () => {
initDatabase()
const { add } = useSnackbar()
const { createAsync } = useVaultStore()
const onCreateAsync = async () => {
@ -121,7 +111,7 @@ const onCreateAsync = async () => {
}
} catch (error) {
console.error(error)
toast({ type: 'error', text: JSON.stringify(error) })
add({ type: 'error', text: JSON.stringify(error) })
}
}
@ -156,4 +146,4 @@ const onClose = () => {
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
}</i18n>
}</i18n>

View File

@ -1,29 +1,16 @@
<template>
<UiDialog
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1"
v-model:open="isOpen"
@click="onLoadDatabase"
>
<!-- @close="initDatabase" -->
<UiDialog v-model:open="isOpen" class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 "
@open="onLoadDatabase">
<template #trigger>
<!-- <button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1"
@click="onLoadDatabase"
> -->
<Icon name="mdi:folder-open-outline" />
{{ t('database.open') }}
<!-- </button> -->
</template>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
@keyup.enter="onOpenDatabase"
autofocus
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
<UiInputPassword :check-input="check" :rules="vaultDatabaseSchema.password" @keyup.enter="onOpenDatabase" autofocus
prepend-icon="mdi:key-outline" v-model="database.password" />
<template #buttons>
<UiButton class="btn-error" @click="onClose">
@ -95,6 +82,7 @@ const onLoadDatabase = async () => {
],
})
console.log("database.path", database.path)
if (!database.path) return
isOpen.value = true
@ -162,8 +150,7 @@ const onClose = () => {
}
</script>
<i18n lang="json">
{
<i18n lang="json">{
"de": {
"open": "Öffnen",
"abort": "Abbrechen",
@ -171,7 +158,6 @@ const onClose = () => {
"open": "Vault öffnen"
}
},
"en": {
"open": "Open",
"abort": "Abort",
@ -179,5 +165,4 @@ const onClose = () => {
"open": "Open Vault"
}
}
}
</i18n>
}</i18n>

16
src/i18n/i18n.config.ts Normal file
View File

@ -0,0 +1,16 @@
/* import de from '@/stores/sidebar/de.json';
import en from '@/stores/sidebar/en.json'; */
export default defineI18nConfig(() => {
return {
legacy: false,
messages: {
de: {
//sidebar: de,
},
en: {
//sidebar: en,
},
},
};
});

View File

@ -1,9 +1,7 @@
<template>
<div class="items-center justify-center min-h-full flex w-full relative">
<div class="fixed top-2 right-2">
<!-- <UiDropdownLocale @select="setLocale" /> -->
<ThemeSwitcher />
<UiDropdownLocale @select="setLocale" />
</div>
<div class="flex flex-col justify-center items-center gap-5 max-w-3xl">
<img src="/logo.svg" class="bg-primary p-3 size-16 rounded-full" alt="HaexVault Logo" />
@ -12,22 +10,13 @@
<p class="whitespace-nowrap">
{{ t('welcome') }}
</p>
<!-- <UiTextGradient>Haex Hub</UiTextGradient> -->
<UiTextGradient>Haex Hub</UiTextGradient>
</span>
<div class="flex flex-col md:flex-row gap-4 w-full h-24 md:h-auto">
<VaultButtonCreate />
<TestDialog />
<BaseDialog />
<!--
<VaultButtonOpen
v-model:isOpen="passwordPromptOpen"
:path="vaultPath"
/> -->
<VaultButtonOpen v-model:isOpen="passwordPromptOpen" :path="vaultPath" />
</div>
<div v-show="lastVaults.length" class="w-full">
@ -60,7 +49,7 @@
<h4>{{ t('sponsors') }}</h4>
<div>
<button @click="openUrl('https://itemis.com')">
<!-- <UiLogoItemis class="text-[#00457C]" /> -->
<UiLogoItemis class="text-[#00457C]" />
</button>
</div>
</div>
@ -97,4 +86,4 @@ await syncLastVaultsAsync()
"lastUsed": "Last used Vaults",
"sponsors": "Powered by"
}
}</i18n>
}</i18n>

View File

@ -0,0 +1,28 @@
import 'flyonui/flyonui'
import type { HSOverlay, IStaticMethods } from 'flyonui/flyonui'
declare global {
interface Window {
HSStaticMethods: IStaticMethods
HSOverlay: typeof HSOverlay
}
}
export default defineNuxtPlugin(() => {
const router = useRouter()
router.afterEach(async () => {
setTimeout(() => {
if (window.HSStaticMethods) {
window.HSStaticMethods.autoInit()
}
}, 50)
})
if (import.meta.client) {
setTimeout(() => {
if (window.HSStaticMethods) {
window.HSStaticMethods.autoInit()
}
}, 50)
}
})

6
src/stores/ui/de.json Normal file
View File

@ -0,0 +1,6 @@
{
"light": "Hell",
"dark": "Dunkel",
"soft": "Soft",
"corporate": "Corporate"
}

6
src/stores/ui/en.json Normal file
View File

@ -0,0 +1,6 @@
{
"light": "Light",
"dark": "Dark",
"soft": "Soft",
"corporate": "Corporate"
}

55
src/stores/ui/index.ts Normal file
View File

@ -0,0 +1,55 @@
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import de from './de.json'
import en from './en.json'
export interface ITheme {
value: string
name: string
icon: string
}
export const useUiStore = defineStore('uiStore', () => {
const breakpoints = useBreakpoints(breakpointsTailwind)
const currentScreenSize = computed(() =>
breakpoints.active().value.length > 0 ? breakpoints.active().value : 'xs'
)
const { t } = useI18n({
messages: {
de: { ui: de },
en: { ui: en },
},
})
const availableThemes = ref([
{
value: 'dark',
name: t('ui.dark'),
icon: 'line-md:moon-rising-alt-loop',
},
{
value: 'light',
name: t('ui.light'),
icon: 'line-md:moon-to-sunny-outline-loop-transition',
},
{ value: 'soft', name: t('ui.soft'), icon: 'line-md:paint-drop' },
{
value: 'corporate',
name: t('ui.corporate'),
icon: 'hugeicons:corporate',
},
])
const defaultTheme = ref(availableThemes.value[0])
const currentTheme = ref(defaultTheme)
return {
availableThemes,
breakpoints,
currentScreenSize,
currentTheme,
defaultTheme,
}
})

10
src/types/flyonui.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import type { IStaticMethods } from 'flyonui/flyonui'
declare global {
interface Window {
// FlyonUI
HSStaticMethods: IStaticMethods
}
}
export {}

121
src/utils/helper.ts Normal file
View File

@ -0,0 +1,121 @@
export const bytesToBase64DataUrlAsync = async (
bytes: Uint8Array,
type = 'application/octet-stream'
) => {
return await new Promise((resolve, reject) => {
const reader = Object.assign(new FileReader(), {
onload: () => resolve(reader.result),
onerror: () => reject(reader.error),
})
reader.readAsDataURL(new File([new Blob([bytes])], '', { type }))
})
}
export const blobToImageAsync = (blob: Blob): Promise<HTMLImageElement> => {
return new Promise((resolve) => {
console.log('transform blob', blob)
const url = URL.createObjectURL(blob)
let img = new Image()
img.onload = () => {
URL.revokeObjectURL(url)
resolve(img)
}
img.src = url
})
}
export const deepToRaw = <T extends Record<string, any>>(sourceObj: T): T => {
const objectIterator = (input: any): any => {
if (Array.isArray(input)) {
return input.map((item) => objectIterator(item))
}
if (isRef(input) || isReactive(input) || isProxy(input)) {
return objectIterator(toRaw(input))
}
if (input && typeof input === 'object') {
return Object.keys(input).reduce((acc, key) => {
acc[key as keyof typeof acc] = objectIterator(input[key])
return acc
}, {} as T)
}
return input
}
return objectIterator(sourceObj)
}
export const readableFileSize = (sizeInByte: number | string = 0) => {
if (!sizeInByte) {
return '0 KB'
}
const size =
typeof sizeInByte === 'string' ? parseInt(sizeInByte) : sizeInByte
const sizeInKb = size / 1024
const sizeInMb = sizeInKb / 1024
const sizeInGb = sizeInMb / 1024
const sizeInTb = sizeInGb / 1024
if (sizeInTb > 1) return `${sizeInTb.toFixed(2)} TB`
if (sizeInGb > 1) return `${sizeInGb.toFixed(2)} GB`
if (sizeInMb > 1) return `${sizeInMb.toFixed(2)} MB`
return `${sizeInKb.toFixed(2)} KB`
}
import type { LocationQueryValue, RouteLocationRawI18n } from 'vue-router'
export const getSingleRouteParam = (
param: string | string[] | LocationQueryValue | LocationQueryValue[]
): string => {
const _param = Array.isArray(param) ? param.at(0) ?? '' : param ?? ''
//console.log('found param', _param, param);
return decodeURIComponent(_param)
}
export const isRouteActive = (
to: RouteLocationRawI18n,
exact: boolean = false
) =>
computed(() => {
const found = useRouter()
.getRoutes()
.find((route) => route.name === useLocaleRoute()(to)?.name)
//console.log('found route', found, useRouter().currentRoute.value, to);
return exact
? found?.name === useRouter().currentRoute.value.name
: found?.name === useRouter().currentRoute.value.name ||
found?.children.some(
(child) => child.name === useRouter().currentRoute.value.name
)
})
export const isKey = <T extends object>(x: T, k: PropertyKey): k is keyof T => {
return k in x
}
export const filterAsync = async <T>(
arr: T[],
predicate: (value: T, index: number, array: T[]) => Promise<boolean>
) => {
// 1. Mappe jedes Element auf ein Promise, das zu true/false auflöst
const results = await Promise.all(arr.map(predicate))
// 2. Filtere das ursprüngliche Array basierend auf den Ergebnissen
return arr.filter((_value, index) => results[index])
}
export const stringToHex = (str: string) =>
str
.split('')
.map((char) => char.charCodeAt(0).toString(16).padStart(2, '0'))
.join('') // Join array into a single string
export const hexToString = (hex: string) => {
if (!hex) return ''
const parsedValue = hex
.match(/.{1,2}/g) // Split hex into pairs
?.map((byte) => String.fromCharCode(parseInt(byte, 16))) // Convert hex to char
.join('') // Join array into a single string
return parsedValue ? parsedValue : ''
}