refactored design

This commit is contained in:
2025-10-28 14:16:17 +01:00
parent 16b71d9ea8
commit ef225b281f
34 changed files with 2574 additions and 819 deletions

View File

@ -1,9 +1,7 @@
<template>
<UApp :locale="locales[locale]">
<div data-vaul-drawer-wrapper>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<NuxtPage />
</div>
</UApp>
</template>

View File

@ -13,6 +13,20 @@
[disabled] {
@apply cursor-not-allowed;
}
#__nuxt {
/* Stellt sicher, dass die App immer die volle Höhe hat */
min-height: 100vh;
/* Sorgt dafür, dass Padding die Höhe nicht sprengt */
@apply box-border;
/* Die Safe-Area Paddings */
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
}
@theme {

View File

@ -1,7 +1,7 @@
<template>
<div
ref="desktopEl"
class="w-full h-full relative overflow-hidden isolate"
class="w-full h-full relative overflow-hidden"
>
<Swiper
:modules="[SwiperNavigation]"
@ -24,7 +24,7 @@
class="w-full h-full"
>
<div
class="w-full h-full relative isolate"
class="w-full h-full relative"
@click.self.stop="handleDesktopClick"
@mousedown.left.self="handleAreaSelectStart"
@dragover.prevent="handleDragOver"
@ -51,7 +51,6 @@
class="absolute right-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
:class="showRightSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
/>
<!-- </Transition> -->
<!-- Area Selection Box -->
<div
@ -199,52 +198,6 @@
<!-- Window Overview Modal -->
<HaexWindowOverview />
<!-- Workspace Drawer -->
<UDrawer
v-model:open="isOverviewMode"
direction="left"
:dismissible="false"
:overlay="false"
:modal="false"
title="Workspaces"
description="Workspaces"
>
<template #content>
<div class="p-6 h-full overflow-y-auto">
<UButton
block
trailing-icon="mdi-close"
class="text-2xl font-bold ext-gray-900 dark:text-white mb-4"
@click="isOverviewMode = false"
>
Workspaces
</UButton>
<!-- Workspace Cards -->
<div class="flex flex-col gap-3">
<HaexWorkspaceCard
v-for="workspace in workspaces"
:key="workspace.id"
:workspace
/>
</div>
<!-- Add New Workspace Button -->
<UButton
block
variant="outline"
class="mt-6"
@click="handleAddWorkspace"
>
<template #leading>
<UIcon name="i-heroicons-plus" />
</template>
New Workspace
</UButton>
</div>
</template>
</UDrawer>
</div>
</template>
@ -600,17 +553,6 @@ const onSlideChange = (swiper: SwiperType) => {
)
}
// Workspace control handlers
const handleAddWorkspace = async () => {
await workspaceStore.addWorkspaceAsync()
// Swiper will auto-slide to new workspace because we switch in addWorkspaceAsync
nextTick(() => {
if (swiperInstance.value) {
swiperInstance.value.slideTo(workspaces.value.length - 1)
}
})
}
/* const handleRemoveWorkspace = async () => {
if (!currentWorkspace.value || workspaces.value.length <= 1) return

View File

@ -5,7 +5,7 @@
:description="vault.path || path"
@confirm="onOpenDatabase"
>
<!-- <UiButton
<UiButton
:label="t('vault.open')"
:ui="{
base: 'px-3 py-2',
@ -14,8 +14,7 @@
size="xl"
variant="outline"
block
@click.stop="onLoadDatabase"
/> -->
/>
<template #title>
<i18n-t

View File

@ -0,0 +1,83 @@
<template>
<UTooltip :text="tooltip">
<button
class="size-8 shrink-0 rounded-lg flex justify-center transition-colors group"
:class="variantClasses.buttonClass"
@click="(e) => $emit('click', e)"
>
<UIcon
:name="icon"
class="size-4 text-gray-600 dark:text-gray-400"
:class="variantClasses.iconClass"
/>
</button>
</UTooltip>
</template>
<script setup lang="ts">
const props = defineProps<{
variant: 'close' | 'maximize' | 'minimize'
isMaximized?: boolean
}>()
defineEmits(['click'])
const icon = computed(() => {
switch (props.variant) {
case 'close':
return 'i-heroicons-x-mark'
case 'maximize':
return props.isMaximized
? 'i-heroicons-arrows-pointing-in'
: 'i-heroicons-arrows-pointing-out'
default:
return 'i-heroicons-minus'
}
})
const variantClasses = computed(() => {
if (props.variant === 'close') {
return {
iconClass: 'group-hover:text-error',
buttonClass: 'hover:bg-error/30 items-center',
}
} else if (props.variant === 'maximize') {
return {
iconClass: 'group-hover:text-warning',
buttonClass: 'hover:bg-warning/30 items-center',
}
} else {
return {
iconClass: 'group-hover:text-success',
buttonClass: 'hover:bg-success/30 items-end pb-1',
}
}
})
const { t } = useI18n()
const tooltip = computed(() => {
switch (props.variant) {
case 'close':
return t('close')
case 'maximize':
return props.isMaximized ? t('shrink') : t('maximize')
default:
return t('minimize')
}
})
</script>
<i18n lang="yaml">
de:
close: Schließen
maximize: Maximieren
shrink: Verkleinern
minimize: Minimieren
en:
close: Close
maximize: Maximize
shrink: Shrink
minimize: Minimize
</i18n>

View File

@ -38,37 +38,21 @@
<!-- Right: Window Controls -->
<div class="flex items-center gap-1 justify-end">
<button
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
<HaexWindowButton
variant="minimize"
@click.stop="handleMinimize"
>
<UIcon
name="i-heroicons-minus"
class="w-4 h-4 text-gray-600 dark:text-gray-400"
/>
</button>
<button
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
/>
<HaexWindowButton
:is-maximized
variant="maximize"
@click.stop="handleMaximize"
>
<UIcon
:name="
isMaximized
? 'i-heroicons-arrows-pointing-in'
: 'i-heroicons-arrows-pointing-out'
"
class="w-4 h-4 text-gray-600 dark:text-gray-400"
/>
</button>
<button
class="w-8 h-8 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 flex items-center justify-center transition-colors group"
/>
<HaexWindowButton
variant="close"
@click.stop="handleClose"
>
<UIcon
name="i-heroicons-x-mark"
class="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-red-600 dark:group-hover:text-red-400"
/>
</button>
/>
</div>
</div>

View File

@ -1,98 +0,0 @@
<template>
<div class="flex flex-col w-full h-full overflow-hidden">
<div ref="headerRef">
<UPageHeader
as="header"
:ui="{
root: [
'bg-default border-b border-accented sticky top-0 z-50 pt-2 px-8 h-header',
],
wrapper: [
'flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4',
],
}"
>
<template #title>
<div class="flex items-center">
<UiLogoHaexhub class="size-12 shrink-0" />
<NuxtLinkLocale
class="link text-base-content link-neutral text-xl font-semibold no-underline flex items-center"
:to="{ name: 'desktop' }"
>
<UiTextGradient class="text-nowrap">
{{ currentVaultName }}
</UiTextGradient>
</NuxtLinkLocale>
</div>
</template>
<template #links>
<UButton
color="neutral"
variant="outline"
:block="isSmallScreen"
icon="i-bi-person-workspace"
size="lg"
@click="isOverviewMode = !isOverviewMode"
/>
<UButton
color="neutral"
variant="outline"
:block="isSmallScreen"
icon="i-heroicons-squares-2x2"
size="lg"
@click="showWindowOverview = !showWindowOverview"
>
<template v-if="openWindowsCount > 0" #trailing>
<UBadge
:label="openWindowsCount.toString()"
color="primary"
size="xs"
/>
</template>
</UButton>
<HaexExtensionLauncher :block="isSmallScreen" />
</template>
</UPageHeader>
</div>
<main class="flex-1 overflow-hidden bg-elevated">
<NuxtPage />
</main>
</div>
</template>
<script setup lang="ts">
const { currentVaultName } = storeToRefs(useVaultStore())
const { isSmallScreen } = storeToRefs(useUiStore())
const { isOverviewMode } = storeToRefs(useWorkspaceStore())
const { showWindowOverview, openWindowsCount } = storeToRefs(
useWindowManagerStore(),
)
</script>
<i18n lang="yaml">
de:
vault:
close: Vault schließen
sidebar:
close: Sidebar ausblenden
show: Sidebar anzeigen
search:
label: Suche
en:
vault:
close: Close vault
sidebar:
close: close sidebar
show: show sidebar
search:
label: Search
</i18n>

View File

@ -1,5 +1,145 @@
<template>
<div class="bg-default isolate w-dvw h-dvh flex flex-col">
<slot />
<div class="w-dvw h-dvh flex flex-col">
<UPageHeader
as="header"
:ui="{
root: ['px-8 py-0'],
wrapper: ['flex flex-row items-center justify-between gap-4'],
}"
>
<template #default>
<div class="flex justify-between items-center py-1">
<div>
<!-- <NuxtLinkLocale
class="link text-base-content link-neutral text-xl font-semibold no-underline flex items-center"
:to="{ name: 'desktop' }"
>
<UiTextGradient class="text-nowrap">
{{ currentVaultName }}
</UiTextGradient>
</NuxtLinkLocale> -->
<UiButton
v-if="currentVaultId"
color="neutral"
variant="outline"
icon="i-bi-person-workspace"
size="lg"
:tooltip="t('header.workspaces')"
@click="isOverviewMode = !isOverviewMode"
/>
</div>
<div>
<div v-if="!currentVaultId">
<UiDropdownLocale @select="onSelectLocale" />
</div>
<div
v-else
class="flex flex-row gap-2"
>
<UButton
v-if="openWindowsCount > 0"
color="primary"
variant="outline"
size="lg"
@click="showWindowOverview = !showWindowOverview"
>
{{ openWindowsCount }}
</UButton>
<HaexExtensionLauncher />
</div>
</div>
</div>
</template>
</UPageHeader>
<main class="flex-1 overflow-hidden bg-elevated flex flex-col">
<slot />
</main>
<!-- Workspace Drawer -->
<UDrawer
v-model:open="isOverviewMode"
direction="left"
:dismissible="false"
:overlay="false"
:modal="false"
title="Workspaces"
description="Workspaces"
>
<template #content>
<div class="p-6 h-full overflow-y-auto">
<UButton
block
trailing-icon="mdi-close"
class="text-2xl font-bold ext-gray-900 dark:text-white mb-4"
@click="isOverviewMode = false"
>
Workspaces
</UButton>
<!-- Workspace Cards -->
<div class="flex flex-col gap-3">
<HaexWorkspaceCard
v-for="workspace in workspaces"
:key="workspace.id"
:workspace
/>
</div>
<!-- Add New Workspace Button -->
<UButton
block
variant="outline"
class="mt-6"
@click="handleAddWorkspace"
>
<template #leading>
<UIcon name="i-heroicons-plus" />
</template>
New Workspace
</UButton>
</div>
</template>
</UDrawer>
</div>
</template>
<script setup lang="ts">
import type { Locale } from 'vue-i18n'
const { t, setLocale } = useI18n()
const onSelectLocale = async (locale: Locale) => {
await setLocale(locale)
}
const { currentVaultId } = storeToRefs(useVaultStore())
const { showWindowOverview, openWindowsCount } = storeToRefs(
useWindowManagerStore(),
)
const workspaceStore = useWorkspaceStore()
const { workspaces, isOverviewMode } = storeToRefs(workspaceStore)
const handleAddWorkspace = async () => {
const workspace = await workspaceStore.addWorkspaceAsync()
nextTick(() => {
workspaceStore.slideToWorkspace(workspace?.id)
})
}
</script>
<i18n lang="yaml">
de:
search:
label: Suche
header:
workspaces: Workspaces
en:
search:
label: Search
header:
workspaces: Workspaces
</i18n>

View File

@ -1,111 +1,132 @@
<template>
<div class="items-center justify-center flex w-full h-full relative">
<div class="absolute top-8 right-8 sm:top-4 sm:right-4">
<UiDropdownLocale @select="onSelectLocale" />
</div>
<div class="flex flex-col justify-center items-center gap-5 max-w-3xl">
<UiLogoHaexhub class="bg-primary p-3 size-16 rounded-full shrink-0" />
<span
class="flex flex-wrap font-bold text-pretty text-xl gap-2 justify-center"
<div>
<NuxtLayout>
<UDashboardPanel
id="inbox-1"
resizable
class=""
>
<p class="whitespace-nowrap">
{{ t('welcome') }}
</p>
<UiTextGradient>Haex Hub</UiTextGradient>
</span>
<template #body>
<div class="items-center justify-center flex relative flex-1">
<!-- <div class="absolute top-0 right-0">
<UiDropdownLocale @select="onSelectLocale" />
</div> -->
<div class="flex flex-col md:flex-row gap-4 w-full h-24 md:h-auto">
<HaexVaultCreate />
<HaexVaultOpen
v-model:open="passwordPromptOpen"
:path="selectedVault?.path"
/>
</div>
<div
v-show="lastVaults.length"
class="w-full"
>
<div class="font-thin text-sm justify-start px-2 pb-1">
{{ t('lastUsed') }}
</div>
<div
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border overflow-scroll"
>
<div
v-for="vault in lastVaults"
:key="vault.name"
class="flex items-center justify-between group overflow-x-scroll"
>
<UButton
variant="ghost"
color="neutral"
class="flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full px-3"
@click="
() => {
passwordPromptOpen = true
selectedVault = vault
}
"
<div
class="flex flex-col justify-center items-center gap-5 max-w-3xl"
>
<span class="block">
{{ vault.name }}
</span>
</UButton>
<UButton
color="error"
square
class="absolute right-2 hidden group-hover:flex min-w-6"
>
<Icon
name="mdi:trash-can-outline"
@click="prepareRemoveVault(vault.name)"
<UiLogoHaexhub
class="bg-primary p-3 size-16 rounded-full shrink-0"
/>
</UButton>
</div>
</div>
</div>
<span
class="flex flex-wrap font-bold text-pretty text-xl gap-2 justify-center"
>
<p class="whitespace-nowrap">
{{ t('welcome') }}
</p>
<UiTextGradient>Haex Hub</UiTextGradient>
</span>
<div class="flex flex-col items-center gap-2">
<h4>{{ t('sponsors') }}</h4>
<div>
<UButton
variant="link"
@click="openUrl('https://itemis.com')"
>
<UiLogoItemis class="text-[#00457C]" />
</UButton>
</div>
</div>
</div>
<UiDialogConfirm
v-model:open="showRemoveDialog"
:title="t('remove.title')"
:description="t('remove.description', { vaultName: vaultToBeRemoved })"
@confirm="onConfirmRemoveAsync"
/>
<div
class="flex flex-col md:flex-row gap-4 w-full h-24 md:h-auto"
>
<HaexVaultCreate />
<HaexVaultOpen
v-model:open="passwordPromptOpen"
:path="selectedVault?.path"
/>
</div>
<div
v-show="lastVaults.length"
class="w-full"
>
<div class="font-thin text-sm justify-start px-2 pb-1">
{{ t('lastUsed') }}
</div>
<div
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border overflow-scroll"
>
<div
v-for="vault in lastVaults"
:key="vault.name"
class="flex items-center justify-between group overflow-x-scroll"
>
<UButton
variant="ghost"
color="neutral"
class="flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full px-3"
@click="
() => {
passwordPromptOpen = true
selectedVault = vault
}
"
>
<span class="block">
{{ vault.name }}
</span>
</UButton>
<UButton
color="error"
square
class="absolute right-2 hidden group-hover:flex min-w-6"
>
<Icon
name="mdi:trash-can-outline"
@click="prepareRemoveVault(vault.name)"
/>
</UButton>
</div>
</div>
</div>
<div class="flex flex-col items-center gap-2">
<h4>{{ t('sponsors') }}</h4>
<div>
<UButton
variant="link"
@click="openUrl('https://itemis.com')"
>
<UiLogoItemis class="text-[#00457C]" />
</UButton>
</div>
</div>
</div>
<UiDialogConfirm
v-model:open="showRemoveDialog"
:title="t('remove.title')"
:description="
t('remove.description', { vaultName: vaultToBeRemoved })
"
@confirm="onConfirmRemoveAsync"
/>
</div>
</template>
</UDashboardPanel>
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
import { openUrl } from '@tauri-apps/plugin-opener'
import type { Locale } from 'vue-i18n'
import type { VaultInfo } from '@bindings/VaultInfo'
definePageMeta({
name: 'vaultOpen',
})
const { t, setLocale } = useI18n()
const { t } = useI18n()
const passwordPromptOpen = ref(false)
const selectedVault = ref<VaultInfo>()
const showRemoveDialog = ref(false)
const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
const { lastVaults } = storeToRefs(useLastVaultStore())
const vaultToBeRemoved = ref('')
@ -127,17 +148,18 @@ const onConfirmRemoveAsync = async () => {
})
}
}
const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
const { syncDeviceIdAsync } = useDeviceStore()
onMounted(async () => {
try {
await syncLastVaultsAsync()
await syncDeviceIdAsync()
} catch (error) {
console.error('ERROR: ', error)
}
})
const onSelectLocale = async (locale: Locale) => {
await setLocale(locale)
}
</script>
<i18n lang="yaml">

View File

@ -1,6 +1,6 @@
<template>
<div class="w-full h-full overflow-y-auto">
<NuxtLayout name="app">
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
@ -48,7 +48,7 @@ const newDeviceName = ref<string>('unknown')
const { readNotificationsAsync } = useNotificationStore()
const { isKnownDeviceAsync } = useDeviceStore()
const { loadExtensionsAsync } = useExtensionsStore()
const { setDeviceIdIfNotExistsAsync, addDeviceNameAsync } = useDeviceStore()
const { addDeviceNameAsync } = useDeviceStore()
const { deviceId } = storeToRefs(useDeviceStore())
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
useVaultSettingsStore()
@ -56,11 +56,11 @@ const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
onMounted(async () => {
try {
// Sync settings first before other initialization
await Promise.allSettled([
syncLocaleAsync(),
syncThemeAsync(),
syncVaultNameAsync(),
setDeviceIdIfNotExistsAsync(),
loadExtensionsAsync(),
readNotificationsAsync(),
])

View File

@ -10,6 +10,7 @@ export type IWorkspace = SelectHaexWorkspaces
export const useWorkspaceStore = defineStore('workspaceStore', () => {
const vaultStore = useVaultStore()
const windowStore = useWindowManagerStore()
const { deviceId } = storeToRefs(useDeviceStore())
const { currentVault } = storeToRefs(vaultStore)
@ -31,10 +32,16 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
return
}
if (!deviceId.value) {
console.error('Keine DeviceId vergeben')
return
}
try {
const items = await currentVault.value.drizzle
.select()
.from(haexWorkspaces)
.where(eq(haexWorkspaces.deviceId, deviceId.value))
.orderBy(asc(haexWorkspaces.position))
workspaces.value = items
@ -58,11 +65,16 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
throw new Error('Kein Vault geöffnet')
}
if (!deviceId.value) {
return
}
try {
const newIndex = workspaces.value.length + 1
const newWorkspace = {
name: name || `Workspace ${newIndex}`,
position: workspaces.value.length,
deviceId: deviceId.value,
}
const result = await currentVault.value.drizzle

View File

@ -4,8 +4,18 @@ import {
platform as tauriPlatform,
} from '@tauri-apps/plugin-os'
export const useDeviceStore = defineStore('vaultInstanceStore', () => {
const deviceId = ref<string>()
const deviceIdKey = 'deviceId'
const defaultDeviceFileName = 'device.json'
export const useDeviceStore = defineStore('vaultDeviceStore', () => {
const deviceId = ref<string | undefined>('')
const syncDeviceIdAsync = async () => {
deviceId.value = await getDeviceIdAsync()
if (deviceId.value) return deviceId.value
deviceId.value = await setDeviceIdAsync()
}
const platform = computedAsync(() => tauriPlatform())
@ -15,7 +25,7 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
const getDeviceIdAsync = async () => {
const store = await getStoreAsync()
return store.get<string>('id')
return await store.get<string>(deviceIdKey)
}
const getStoreAsync = async () => {
@ -23,30 +33,19 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
public: { haexVault },
} = useRuntimeConfig()
return await load(haexVault.instanceFileName || 'instance.json')
return await load(haexVault.deviceFileName || defaultDeviceFileName)
}
const setDeviceIdAsync = async (id?: string) => {
const store = await getStoreAsync()
const _id = id || crypto.randomUUID()
await store.set('id', _id)
deviceId.value = _id
await store.set(deviceIdKey, _id)
return _id
}
const setDeviceIdIfNotExistsAsync = async () => {
const _deviceId = await getDeviceIdAsync()
if (_deviceId) {
deviceId.value = _deviceId
return deviceId.value
}
return await setDeviceIdAsync()
}
const isKnownDeviceAsync = async () => {
const { readDeviceNameAsync } = useVaultSettingsStore()
const deviceId = await getDeviceIdAsync()
return deviceId ? (await readDeviceNameAsync(deviceId)) || false : false
return !!(await readDeviceNameAsync(deviceId.value))
}
const readDeviceNameAsync = async (id?: string) => {
@ -99,12 +98,13 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
addDeviceNameAsync,
deviceId,
deviceName,
getDeviceIdAsync,
hostname,
isKnownDeviceAsync,
platform,
readDeviceNameAsync,
setDeviceIdAsync,
setDeviceIdIfNotExistsAsync,
syncDeviceIdAsync,
updateDeviceNameAsync,
}
})

View File

@ -118,9 +118,11 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
.where(eq(schema.haexSettings.key, 'vaultName'))
}
const readDeviceNameAsync = async (id: string) => {
const readDeviceNameAsync = async (id?: string) => {
const { currentVault } = useVaultStore()
if (!id) return undefined
const deviceName =
await currentVault?.drizzle?.query.haexSettings.findFirst({
where: and(