switch to nuxt ui

This commit is contained in:
2025-09-11 00:58:55 +02:00
parent 3975d26caa
commit 0a7de8b78b
143 changed files with 19019 additions and 9899 deletions

View File

@ -1,48 +0,0 @@
<template>
<div
class="accordion divide-neutral/20 divide-y accordion-shadow *:accordion-item-active:shadow-md"
>
<div
:id="itemId"
ref="accordionRef"
class="accordion-item active"
>
<button
class="accordion-toggle inline-flex items-center gap-x-4 text-start"
:aria-controls="collapseId"
aria-expanded="true"
type="button"
>
<span
class="icon-[tabler--chevron-right] accordion-item-active:rotate-90 size-5 shrink-0 transition-transform duration-300 rtl:rotate-180"
/>
<slot name="title" />
</button>
<div
:id="collapseId"
class="accordion-content w-full overflow-hidden transition-[height] duration-300"
:aria-labelledby="itemId"
role="region"
>
<slot />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { HSAccordion } from 'flyonui/flyonui'
const itemId = useId()
const collapseId = useId()
const accordionRef = useTemplateRef('accordionRef')
const accordion = ref<HSAccordion>()
onMounted(() => {
if (accordionRef.value) {
accordion.value = new window.HSAccordion(accordionRef.value)
accordion.value.hide()
}
})
</script>

View File

@ -1,107 +0,0 @@
<template>
<div class="z-10 pointer-events-auto">
<div
class="dropdown relative inline-flex [--placement:top] [--strategy:absolute]"
>
<button
:id
class="dropdown-toggle btn btn-primary btn-xl btn-square dropdown-open:rotate-45 transition-transform"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Menu"
>
<Icon
:name="icon"
class="size-11 shrink-0"
/>
</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 py-1"
>
<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"
/>
{{ te(link.label) ? t(link.label) : 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()
const { t, te } = useI18n()
</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>

View File

@ -1,22 +1,31 @@
<template>
<button
class="btn join-item pointer-events-auto"
:type
>
<UiTooltip
:tooltip
v-if="tooltip"
>
<slot />
</UiTooltip>
<slot v-else />
</button>
<div>
<UTooltip :text="buttonProps?.tooltip">
<UButton
class="pointer-events-auto"
v-bind="{ ...buttonProps, ...$attrs }"
@click="(e) => $emit('click', e)"
>
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
/>
</template>
</UButton>
</UTooltip>
</div>
</template>
<script setup lang="ts">
const { type = 'button' } = defineProps<{
type?: 'reset' | 'submit' | 'button'
import type { ButtonProps } from '@nuxt/ui'
interface IButtonProps extends /* @vue-ignore */ ButtonProps {
tooltip?: string
}>()
}
const buttonProps = defineProps<IButtonProps>()
defineEmits<{ click: [Event] }>()
</script>

View File

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

View File

@ -1,81 +0,0 @@
<template>
<div class="card min-w-56">
<slot name="image" />
<div
class="card-header"
v-if="$slots.title || title"
>
<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"
:class="bodyClass"
>
<slot />
<div
v-if="$slots.action"
class="card-actions"
>
<slot name="action" />
</div>
</div>
<div
v-if="$slots.footer"
class="card-footer"
>
<slot name="footer" />
</div>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['close', 'submit'])
defineProps<{
title?: string
subtitle?: string
icon?: string
bodyClass?: 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

@ -1,61 +1,73 @@
<template>
<UiDialog
:title
@close="onAbort"
<UModal
v-model:open="open"
:title
:description
:fullscreen="isSmallScreen"
>
<template #trigger>
<slot name="trigger" />
</template>
<slot>
<!-- <UiButton
color="primary"
variant="outline"
icon="mdi:menu"
:ui="{
base: '',
}"
/> -->
</slot>
<template #title>
<slot name="title" />
</template>
<slot />
<template #buttons>
<slot name="buttons">
<UiButton
class="btn-error btn-outline w-full sm:w-auto"
@click="onAbort"
>
<Icon :name="abortIcon || 'mdi:close'" />
{{ abortLabel ?? t('abort') }}
</UiButton>
<UiButton
class="btn-primary w-full sm:w-auto"
@click="onConfirm"
>
<Icon :name="confirmIcon || 'mdi:check'" />
{{ confirmLabel ?? t('confirm') }}
</UiButton>
</slot>
<template #body>
<slot name="body" />
</template>
</UiDialog>
<template #footer>
<div class="flex flex-col sm:flex-row gap-4 justify-end w-full">
<UiButton
:icon="abortIcon || 'mdi:close'"
:label="abortLabel || t('abort')"
block
color="error"
variant="outline"
@click="open = false"
/>
<UiButton
:icon="confirmIcon || 'mdi:check'"
:label="confirmLabel || t('confirm')"
block
color="primary"
varaint="solid"
@click="$emit('confirm')"
/>
</div>
</template>
</UModal>
</template>
<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
defineProps<{
confirmLabel?: string
abortLabel?: string
title?: string
abortIcon?: string
abortLabel?: string
confirmIcon?: string
confirmLabel?: string
description?: string
title?: string
}>()
const open = defineModel<boolean>('open', { default: false })
const { t } = useI18n()
const emit = defineEmits(['confirm', 'abort'])
defineEmits(['confirm'])
const onAbort = () => {
emit('abort')
}
const breakpoints = useBreakpoints(breakpointsTailwind)
const onConfirm = () => {
emit('confirm')
}
// "smAndDown" gilt für sm, xs usw.
const isSmallScreen = breakpoints.smaller('sm')
</script>
<i18n lang="yaml">

View File

@ -1,116 +0,0 @@
<template>
<button
v-if="$slots.trigger || label"
v-bind="$attrs"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
:aria-label="label"
@click="$emit('open')"
>
<slot name="trigger">
{{ label }}
</slot>
</button>
<div class="hidden">
<Teleport to="body">
<div
:id
ref="modalRef"
class="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden modal-middle p-0 xs:p-2 --prevent-on-load-init pointer-events-auto max-w-none"
role="dialog"
tabindex="-1"
>
<div
class="overlay-animation-target overlay-open:duration-300 overlay-open:opacity-100 transition-all ease-out modal-dialog"
>
<div class="modal-content justify-between">
<div class="modal-header py-0 sm:py-4">
<div
v-if="title || $slots.title"
class="modal-title py-4 break-all"
>
<slot name="title">
{{ title }}
</slot>
</div>
<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 grow mt-0 pt-0">
<slot />
</div>
<div class="modal-footer flex-col sm:flex-row">
<slot name="buttons" />
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import type { HSOverlay } from 'flyonui/flyonui'
const { currentTheme } = storeToRefs(useUiStore())
defineProps<{ title?: string; label?: string }>()
const emit = 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 () => {
if (!modal.value) return
if (open.value) {
await modal.value.open()
} else {
await modal.value.close(true)
emit('close')
}
})
onMounted(async () => {
if (!modalRef.value) return
modal.value = new window.HSOverlay(modalRef.value)
modal.value.isLayoutAffect = true
modal.value.on('close', () => {
open.value = false
})
})
</script>
<i18n lang="yaml">
de:
close: Schließen
en:
close: Close
</i18n>

View File

@ -1,55 +0,0 @@
<template>
<UiDialogConfirm
:confirm-label="t('apply')"
:title="t('title')"
@abort="open = false"
@click="open = true"
class="btn btn-square btn-accent btn-outline"
v-model:open="open"
>
<template #trigger>
<Icon name="mdi:dice" />
</template>
<form class="flex flex-col gap-4">
<UiInputPassword
v-model="newPassword"
prepend-icon="mdi:key-outline"
with-copy-button
>
<template #append>
<UiButton class="btn-square btn-accent btn-outline">
<Icon name="mdi:refresh" />
</UiButton>
</template>
</UiInputPassword>
</form>
</UiDialogConfirm>
</template>
<script setup lang="ts">
const open = defineModel<boolean>()
const { t } = useI18n()
const { password } = defineProps<{
autofocus?: boolean
checkInput?: boolean
label?: string
placeholder?: string
withCopyButton?: boolean
password: string | null
}>()
const newPassword = computed(() => password)
</script>
<i18n lang="yaml">
de:
title: Passwortgenerator
apply: Übernehmen
en:
title: Passwordgenerator
apply: Apply
</i18n>

View File

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

View File

@ -1,40 +1,38 @@
<template>
<UiDropdown
:items="availableLocales"
class="btn btn-primary btn-outline"
@select="(locale) => $emit('select', locale)"
<UDropdownMenu
arrow
:items
:ui="{}"
>
<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>
<UButton
:icon="items.find((item) => item.label === locale)?.icon"
:label="locale"
color="neutral"
variant="outline"
/>
</UDropdownMenu>
</template>
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
import type { Locale } from 'vue-i18n'
const { locales, locale } = useI18n()
const flags = {
de: 'emojione:flag-for-germany',
en: 'emojione:flag-for-united-kingdom',
de: 'circle-flags:de',
en: 'circle-flags:uk',
}
const { availableLocales, locale } = useI18n()
const emit = defineEmits<{ select: [Locale] }>()
defineEmits<{ select: [Locale] }>()
const items = computed<DropdownMenuItem[]>(() =>
locales.value.map((locale) => ({
label: locale.code,
icon: flags[locale.code],
onSelect() {
emit('select', locale.code)
},
})),
)
</script>

View File

@ -1,22 +1,24 @@
<template>
<UiDropdown :items="availableThemes" class="btn btn-primary btn-outline" @select="(theme) => $emit('select', theme)">
<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>
<UDropdownMenu :items>
<UButton :icon="currentTheme?.icon" />
</UDropdownMenu>
</template>
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
const { availableThemes, currentTheme } = storeToRefs(useUiStore())
defineEmits<{ select: [ITheme] }>()
</script>
const emit = defineEmits<{ select: [string] }>()
watchImmediate(availableThemes, () =>
console.log('availableThemes', availableThemes),
)
const items = computed<DropdownMenuItem[]>(() =>
availableThemes?.value.map((theme) => ({
...theme,
onSelect: () => emit('select', theme.value),
})),
)
</script>

View File

@ -0,0 +1,45 @@
<template>
<UDropdownMenu :items>
<UButton
icon="mdi:menu"
color="neutral"
variant="outline"
/>
</UDropdownMenu>
</template>
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
const { t } = useI18n()
const { closeAsync } = useVaultStore()
const onVaultCloseAsync = async () => {
await closeAsync()
await navigateTo(useLocalePath()({ name: 'vaultOpen' }))
}
const items: DropdownMenuItem[] = [
{
icon: 'tabler:settings',
label: t('settings'),
to: useLocalePath()({ name: 'settings' }),
},
{
icon: 'tabler:logout',
label: t('close'),
onSelect: () => onVaultCloseAsync(),
color: 'error',
},
]
</script>
<i18n lang="yaml">
de:
settings: 'Einstellungen'
close: 'Vault schließen'
en:
settings: 'Settings'
close: 'Close Vault'
</i18n>

View File

@ -1,181 +1,88 @@
<template>
<div>
<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"
/>
<div class="input-floating grow">
<input
:autofocus
:id
:name="name ?? id"
:placeholder="placeholder || label"
:readonly="read_only"
:type
class="ps-2"
ref="inputRef"
v-model="input"
@keyup="(e:KeyboardEvent) => $emit('keyup', e)"
/>
<label
:for="id"
class="input-floating-label"
>
{{ label }}
</label>
</div>
<Icon
v-if="appendIcon"
:name="appendIcon"
class="my-auto shrink-0"
/>
</div>
<UiButton
v-if="withClearButton"
class="btn-outline btn-square"
@click="input = ''"
>
<Icon name="mdi:close" />
</UiButton>
<slot name="append" />
<UiButton
v-if="withCopyButton"
:tooltip="t('copy')"
class="btn-outline btn-accent btn-square"
@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"
<UInput
v-model="value"
:placeholder="props.placeholder || ' '"
:readonly="props.readOnly"
:leading-icon="props.leadingIcon"
:ui="{ base: 'peer' }"
@change="(e) => $emit('change', e)"
@blur="(e) => $emit('blur', e)"
@keyup="(e: KeyboardEvent) => $emit('keyup', e)"
@keydown="(e: KeyboardEvent) => $emit('keydown', e)"
>
<label
class="absolute pointer-events-none -top-2.5 left-0 text-highlighted text-xs font-medium px-1.5 transition-all peer-focus:-top-2.5 peer-focus:text-highlighted peer-focus:text-xs peer-focus:font-medium peer-placeholder-shown:text-sm peer-placeholder-shown:text-dimmed peer-placeholder-shown:top-1.5 peer-placeholder-shown:font-normal"
>
<span
v-for="error in errors"
class="label-text-alt text-error"
class="inline-flex bg-default px-1"
:class="props?.leadingIcon ? 'mx-6' : 'mx-0'"
>
{{ error }}
{{ props?.label }}
</span>
</span>
</div>
</label>
<template #trailing>
<slot name="trailing" />
<UiButton
v-show="props.withCopyButton"
:color="copied ? 'success' : 'neutral'"
:tooltip="t('copy')"
:icon="copied ? 'mdi:check' : 'mdi:content-copy'"
size="sm"
variant="link"
@click="copy(`${value}`)"
/>
</template>
<template
v-for="(_, slotName) in filteredSlots"
#[slotName]="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
/>
</template>
</UInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod'
import type { AcceptableValue, InputProps } from '@nuxt/ui'
const input = defineModel<string | number | undefined | null>({
required: true,
})
const value = defineModel<AcceptableValue | undefined>()
const inputRef = useTemplateRef('inputRef')
defineExpose({ inputRef })
const emit = defineEmits<{
error: [string[]]
keyup: [KeyboardEvent]
}>()
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,
withClearButton: Boolean,
autofocus: Boolean,
read_only: Boolean,
})
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 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 = []
}
}
interface IInputProps extends /* @vue-ignore */ InputProps {
tooltip?: string
}
const props = defineProps<
IInputProps & {
withCopyButton?: boolean
readOnly?: boolean
label?: string
leadingIcon?: string
}
>()
defineEmits<{
change: [Event]
blur: [Event]
keyup: [KeyboardEvent]
keydown: [KeyboardEvent]
}>()
const { copy, copied } = useClipboard()
const { t } = useI18n()
const filteredSlots = computed(() => {
return Object.fromEntries(
Object.entries(useSlots()).filter(([name]) => name !== 'trailing'),
)
})
watchImmediate(props, () => console.log('props', props))
</script>
<i18n lang="yaml">

View File

@ -1,62 +1,53 @@
<template>
<UiInput
v-model="value"
:autofocus
:check-input
:label="label || t('password')"
:placeholder="placeholder || t('password')"
:rules
:type="type"
:label="t('label')"
:leading-icon
:placeholder="placeholder || ' '"
:read-only
:type="show ? 'text' : 'password'"
:with-copy-button
@keyup="(e) => $emit('keyup', e)"
>
<template #append>
<slot name="append" />
<template #trailing>
<UiButton
class="btn-outline btn-accent btn-square join-item"
@click="tooglePasswordType"
>
<Icon :name="type === 'password' ? 'mdi:eye-off' : 'mdi:eye'" />
</UiButton>
aria-controls="password"
color="neutral"
variant="link"
:aria-label="show ? t('hide') : t('show')"
:aria-pressed="show"
:icon="show ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:tooltip="show ? t('hide') : t('show')"
size="sm"
@click="show = !show"
/>
</template>
</UiInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod'
const { t } = useI18n()
const value = defineModel<string | number | null | undefined>()
import type { AcceptableValue } from '@nuxt/ui'
defineProps<{
autofocus?: boolean
checkInput?: boolean
label?: string
placeholder?: string
rules?: ZodSchema
leadingIcon?: string
withCopyButton?: boolean
readOnly?: boolean
}>()
const value = defineModel<AcceptableValue | undefined>()
defineEmits<{
keyup: [KeyboardEvent]
}>()
const type = ref<'password' | 'text'>('password')
const tooglePasswordType = () => {
type.value = type.value === 'password' ? 'text' : 'password'
}
const show = ref(false)
const { t } = useI18n()
</script>
<i18n lang="json">
{
"de": {
"password": "Passwort"
},
"en": {
"password": "Password"
}
}
<i18n lang="yaml">
de:
show: Passwort ansehen
hide: Passwort verstecken
label: Passwort
en:
show: Show password
hide: Hide password
label: Password
</i18n>

View File

@ -1,5 +1,6 @@
<template>
<UiInput
v-model.trim="value"
:autofocus
:check-input="checkInput"
:label="label || t('url')"
@ -7,17 +8,18 @@
:read_only
:rules
:with-copy-button
v-model.trim="value"
@keyup="(e) => $emit('keyup', e)"
>
<template #append>
<template #trailing>
<UiButton
color="neutral"
variant="link"
size="sm"
icon="streamline:web"
:disabled="!value?.length"
:tooltip="t('browse')"
@click="openUrl(`${value}`)"
class="btn-outline btn-accent btn-square"
>
<Icon name="streamline:web" />
</UiButton>
/>
</template>
</UiInput>
</template>
@ -45,13 +47,12 @@ defineEmits<{
}>()
</script>
<i18n lang="json">
{
"de": {
"url": "Url"
},
"en": {
"url": "Url"
}
}
<i18n lang="yaml">
de:
url: Url
browse: Url öffnen
en:
url: Url
browse: Open url
</i18n>

View File

@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col h-fit border border-base-content/25 divide-base-content/25 divide-y rounded-md first:rounded-t-md last:rounded-b-md"
class="flex flex-col h-fit border border-default/25 divide-default/25 divide-y rounded-md first:rounded-t-md last:rounded-b-md"
>
<slot />
</div>

View File

@ -2,7 +2,6 @@
<svg
viewBox="122 107 263 292"
xmlns="http://www.w3.org/2000/svg"
style="max-height: 500px"
>
<g
stroke-width="0.3"

View File

@ -10,24 +10,23 @@
<input
:id
:readonly="read_only"
:disabled="read_only"
ref="colorRef"
v-model="model"
:readonly="readOnly"
:disabled="readOnly"
:title="t('pick')"
class="top-0 left-0 absolute size-0"
type="color"
v-model="model"
ref="colorRef"
/>
<UiTooltip :tooltip="t('reset')">
<button
<UiButton
color="error"
:class="{ 'btn-disabled': readOnly }"
icon="mdi:refresh"
:disabled="readOnly"
@click="model = ''"
class="btn btn-sm text-sm btn-outline btn-error"
:class="{ 'btn-disabled': read_only }"
type="button"
>
<Icon name="mdi:refresh" />
</button>
/>
</UiTooltip>
</div>
</template>
@ -39,12 +38,12 @@ const { t } = useI18n()
const model = defineModel<string | null>()
const colorRef = useTemplateRef('colorRef')
defineProps({
read_only: Boolean,
readOnly: Boolean,
})
const { currentTheme } = storeToRefs(useUiStore())
const textColorClass = computed(() => {
if (!model.value)
if (!model.value && currentTheme.value)
return currentTheme.value.value === 'dark' ? 'text-black' : 'text-white'
const color = getContrastingTextColor(model.value)

View File

@ -1,5 +1,5 @@
<template>
<UiDropdown
<UDropdownMenu
:items="icons"
class="btn"
@select="(newIcon) => (iconName = newIcon)"
@ -23,7 +23,7 @@
</li>
</div>
</template>
</UiDropdown>
</UDropdownMenu>
</template>
<script setup lang="ts">

View File

@ -0,0 +1,60 @@
<template>
<li
class="rounded hover:bg-elevated py-2 cursor-pointer"
:class="{
['bg-base-content/20 ']: isActive,
}"
@click="triggerNavigate"
>
<UTooltip :tooltip="tooltip ?? name">
<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>
</UTooltip>
</li>
</template>
<script setup lang="ts">
import type { ISidebarItem } from '#imports'
const props = defineProps<ISidebarItem>()
const router = useRouter()
console.log('to', props.to)
const isActive = computed(() => {
if (props.to?.name === 'haexExtension') {
return (
getSingleRouteParam(router.currentRoute.value.params.extensionId) ===
props.id
)
} else {
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,
)
)
}
})
const linkRef = useTemplateRef('linkRef')
const triggerNavigate = () => linkRef.value?.$el.click()
</script>

View File

@ -1,7 +0,0 @@
<template>
<input v-model="value" />
</template>
<script setup lang="ts">
const value = defineModel()
</script>

View File

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

View File

@ -1,41 +1,54 @@
<template>
<div class="relative">
<UiButton
v-if="withCopyButton"
:tooltip="t('copy')"
class="btn-square btn-outline btn-accent absolute z-10 top-2 right-2"
@click="copy(`${value}`)"
<div>
<UTextarea
:id
v-model="value"
:ui="{ base: 'peer' }"
:readonly="readOnly"
class="w-full"
v-bind="$attrs"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</UiButton>
<div class="textarea-floating">
<textarea
:class="{ 'pr-10': withCopyButton }"
:id
:placeholder
:readonly="read_only"
class="textarea"
v-bind="$attrs"
v-model="value"
></textarea>
<label
class="textarea-floating-label"
:for="id"
class="absolute pointer-events-none -top-2.5 left-0 text-highlighted text-xs font-medium px-1.5 transition-all peer-focus:-top-2.5 peer-focus:text-highlighted peer-focus:text-xs peer-focus:font-medium peer-placeholder-shown:text-sm peer-placeholder-shown:text-dimmed peer-placeholder-shown:top-1.5 peer-placeholder-shown:font-normal"
>
{{ label }}
<span class="inline-flex bg-default px-1">
{{ props.label }}
</span>
</label>
</div>
<template #trailing>
<UiButton
v-show="withCopyButton"
:color="copied ? 'success' : 'neutral'"
:tooltip="t('copy')"
:icon="copied ? 'mdi:check' : 'mdi:content-copy'"
size="sm"
variant="link"
@click="copy(`${value}`)"
/>
</template>
</UTextarea>
</div>
</template>
<script setup lang="ts">
defineProps<{
import type { TextareaProps } from '@nuxt/ui'
interface ITextareaProps extends /* @vue-ignore */ TextareaProps {
tooltip?: string
withCopyButton?: boolean
readOnly?: boolean
label?: string
}
const props = defineProps<ITextareaProps>()
/* defineProps<{
placeholder?: string
label?: string
read_only?: boolean
readOnly?: boolean
withCopyButton?: boolean
}>()
}>() */
const id = useId()

View File

@ -1,57 +0,0 @@
<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 pointer-events-none z-50"
role="tooltip"
>
<span class="tooltip-body">
{{ 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: {
type: String,
default: '',
},
trigger: {
type: String as PropType<'focus' | 'hover' | 'click'>,
default: 'hover',
},
})
</script>

View File

@ -1,26 +0,0 @@
<template>
<div
class="tree-view-selected:bg-base-200/60 dragged:bg-primary/20 dragged:rounded nested-4 cursor-pointer rounded-md px-2"
role="treeitem" :data-tree-view-item="JSON.stringify({
value,
isDir: false,
})
">
<div class="flex items-center gap-x-3">
<span class="icon-[tabler--file] text-base-content size-4 flex-shrink-0"/>
<div class="grow">
<span class="text-base-content">{{ value }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
value: String,
});
const id = useId();
const controlId = useId();
const isActive = ref(false);
</script>

View File

@ -1,204 +0,0 @@
<template>
<div :id ref="folderRef" data-nested-draggable="" :value class="">
<div
isDir :data-tree-view-item="JSON.stringify({ value })"
class="accordion-item active motion-preset-slide-left motion-ease-spring-bouncier" :class="{
'selected': isActive?.value,
'text-base-content': !color,
}" role="treeitem" :style="{ color: color || '' }">
<div
class="accordion-heading tree-view-selected:bg-primary/80 flex items-center gap-x-0.5 rounded-md hover:bg-primary/20 group">
<button class="accordion-toggle btn btn-sm btn-circle btn-text shrink-0" :aria-controls="controlId">
<Icon name="tabler:plus" class="accordion-item-active:rotate-45 size-4 transition-all duration-300" />
</button>
<button class="cursor-pointer rounded-md px-1.5 w-full" @click.stop="$emit('click', value)">
<div class="flex items-center gap-x-3">
<Icon v-if="icon" :name="icon || 'mdi:folder-outline'" class="shrink-0" />
<div class="flex whitespace-nowrap">
{{ value }}
</div>
</div>
</button>
<button
class="sticky right-2 btn btn-sm btn-circle btn-text shrink-0 group-hover:flex hidden ml-auto"
@click.stop="$emit('edit', value)">
<Icon name="mdi:pencil-outline" class="size-4 transition-all duration-300" />
</button>
</div>
<div
:id="controlId" class="accordion-content w-full transition-[height] duration-300" role="group"
:aria-labelledby="id">
<div ref="childRef" class="tree-view-space min-h-1" data-nested-draggable="">
<slot>
<template
v-for="(item, index) in children?.sort(
(a, b) => a.order ?? 0 - (b.order ?? 0)
)" :key="item.id!" :data-tree-view-item="JSON.stringify({ value: item.value })">
<UiTreeFolder
v-if="item.type === 'folder'" :icon="item.icon || 'tabler:folder'"
v-bind="item" @click="(value) => $emit('click', value)" @edit="(value) => $emit('edit', value)" />
<UiTreeFile v-if="item.type === 'file'" v-bind="item" />
</template>
</slot>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { HSAccordion } from 'flyonui/flyonui';
import Sortable from 'sortablejs';
const props = defineProps({
value: String,
icon: {
type: [String, null],
default: 'tabler:folder',
},
children: {
type: Array as PropType<ITreeItem[] | null>,
default: () => [],
},
name: String,
color: [String, null],
isActive: Object as PropType<ComputedRef<boolean>>,
});
const id = useId();
const controlId = useId();
const folderRef = ref<HTMLElement>();
const childRef = ref<HTMLElement>();
defineEmits<{
click: [value: string | undefined];
edit: [value: string | undefined];
}>();
const { groups } = storeToRefs(useVaultGroupStore());
const sorty = ref([]);
onMounted(() => {
if (folderRef.value && childRef.value)
[folderRef.value, childRef.value].forEach((element) => {
const create = Sortable.create(element, {
animation: 150,
ghostClass: 'bg-opacity-20',
group: 'vault',
swapThreshold: 0.65,
fallbackOnBody: true,
fallbackTolerance: 3,
onEnd: (evt) => {
const { item } = evt;
/* if (item.classList.contains('accordion')) {
let existingInstance = HSAccordion.getInstance(item, true);
let updatedInstance;
existingInstance.element.update();
updatedInstance = HSAccordion.getInstance(item, true);
window.$hsAccordionCollection.map((el) => {
if (
el.element.el !== existingInstance.element.el &&
el.element.group === existingInstance.element.group &&
el.element.el.closest('.accordion') &&
el.element.el.classList.contains('active') &&
existingInstance.element.el.classList.contains('active')
)
el.element.hide();
return el;
});
}
if (!!item.hasAttribute('data-tree-view-item')) {
const treeViewItem = HSTreeView.getInstance(
item.closest('[data-tree-view]'),
true
);
treeViewItem.element.update();
} */
},
onUpdate: (evt) => {
console.log('update', evt.item, props.value, sorty.value);
},
});
/* const sortable = new Sortable(element, {
animation: 150,
ghostClass: 'bg-opacity-20',
group: 'vault',
swapThreshold: 0.65,
fallbackOnBody: true,
fallbackTolerance: 3,
onEnd: (evt) => {
console.log(
'end',
evt.item,
props.value,
sorty.value.at(0).toArray(),
sorty.value.at(1).toArray()
);
},
onUpdate: (evt) => {
console.log('update', evt.item, props.value, sorty.value);
},
});
sorty.value.push(sortable); */
});
});
onMounted(() => {
const draggable = document.querySelectorAll('[data-nested-draggable]');
draggable.forEach((el) => {
const options = {
group: 'nested',
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
ghostClass: 'dragged',
onEnd: (evt) => {
const { item, items } = evt;
console.log('standard', item, evt);
if (item.classList.contains('accordion')) {
const existingInstance = HSAccordion.getInstance(item, true);
let updatedInstance;
existingInstance.element.update();
updatedInstance = HSAccordion.getInstance(item, true);
window.$hsAccordionCollection.map((el) => {
if (
el.element.el !== existingInstance.element.el &&
el.element.group === existingInstance.element.group &&
el.element.el.closest('.accordion') &&
el.element.el.classList.contains('active') &&
existingInstance.element.el.classList.contains('active')
)
el.element.hide();
return el;
});
}
if (item.hasAttribute('data-tree-view-item')) {
const treeViewItem = HSTreeView.getInstance(
item.closest('[data-tree-view]'),
true
);
treeViewItem.element.update();
}
},
};
const data = el.getAttribute('data-nested-draggable');
const dataOptions = data ? JSON.parse(data) : {};
const sortable = new Sortable(el, options);
console.log('stand', sortable.toArray());
});
});
</script>

View File

@ -1,5 +0,0 @@
<template>
<div data-tree-view role="tree" aria-orientation="vertical" class="rounded min-w-fit w-full">
<slot/>
</div>
</template>

View File

@ -1,19 +0,0 @@
interface ITreeItem {
id: string | null
value: string
name: string | null
icon?: string | null
children?: ITreeItem[] | null
type: 'folder' | 'file'
color?: string | null
order?: number | null
parentId?: string | null
}
interface IVaultGroupTreeItem {
id?: string | null
name: string
icon?: string | null
children?: IVaultGroupTreeItem[] | null
desciption?: string | null
}