mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
Fix Android safe-area handling and window maximization
- Fix extension signature verification on Android by canonicalizing paths (symlink compatibility) - Implement proper safe-area-inset handling for mobile devices - Add reactive header height measurement to UI store - Fix maximized window positioning to respect safe-areas and header - Create reusable HaexDebugOverlay component for mobile debugging - Fix Swiper navigation by using absolute positioning instead of flex-1 - Remove debug logging after Android compatibility confirmed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -14,18 +14,44 @@
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
|
||||
/* Define safe-area-insets as CSS custom properties for JavaScript access */
|
||||
:root {
|
||||
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||
}
|
||||
|
||||
/* Verhindere Scrolling auf html und body */
|
||||
html {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100dvh;
|
||||
height: 100vh; /* Fallback */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#__nuxt {
|
||||
/* Stellt sicher, dass die App immer die volle Höhe hat */
|
||||
min-height: 100vh;
|
||||
/* Volle Höhe des body */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
/* Sorgt dafür, dass Padding die Höhe nicht sprengt */
|
||||
@apply box-border;
|
||||
/* Safe-Area Paddings auf root element - damit ALLES davon profitiert */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
|
||||
/* 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);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
61
src/components/haex/debug/overlay.vue
Normal file
61
src/components/haex/debug/overlay.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="data"
|
||||
class="fixed top-2 right-2 bg-black/90 text-white text-xs p-3 rounded-lg shadow-2xl max-w-sm z-[9999] backdrop-blur-sm"
|
||||
>
|
||||
<div class="flex justify-between items-start gap-3 mb-2">
|
||||
<span class="font-bold text-sm">{{ title }}</span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
|
||||
@click="copyToClipboardAsync"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
v-if="dismissible"
|
||||
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
|
||||
@click="handleDismiss"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="text-xs whitespace-pre-wrap font-mono overflow-auto max-h-96">{{ formattedData }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: Record<string, any> | null
|
||||
title?: string
|
||||
dismissible?: boolean
|
||||
}>(),
|
||||
{
|
||||
title: 'Debug Info',
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
dismiss: []
|
||||
}>()
|
||||
|
||||
const formattedData = computed(() => {
|
||||
if (!props.data) return ''
|
||||
return JSON.stringify(props.data, null, 2)
|
||||
})
|
||||
|
||||
const copyToClipboardAsync = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(formattedData.value)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy debug info:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
emit('dismiss')
|
||||
}
|
||||
</script>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="desktopEl"
|
||||
class="w-full h-full relative overflow-hidden"
|
||||
class="absolute inset-0 overflow-hidden"
|
||||
>
|
||||
<Swiper
|
||||
:modules="[SwiperNavigation]"
|
||||
@ -13,7 +13,7 @@
|
||||
:no-swiping="true"
|
||||
no-swiping-class="no-swipe"
|
||||
:allow-touch-move="allowSwipe"
|
||||
class="w-full h-full"
|
||||
class="h-full w-full"
|
||||
direction="vertical"
|
||||
@swiper="onSwiperInit"
|
||||
@slide-change="onSlideChange"
|
||||
@ -115,7 +115,14 @@
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="window.type === 'extension' && availableExtensions.find(ext => ext.id === window.sourceId)?.devServerUrl ? 'warning' : undefined"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find(
|
||||
(ext) => ext.id === window.sourceId,
|
||||
)?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@ -165,7 +172,13 @@
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="window.type === 'extension' && availableExtensions.find(ext => ext.id === window.sourceId)?.devServerUrl ? 'warning' : undefined"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find((ext) => ext.id === window.sourceId)
|
||||
?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
v-if="preview?.manifest.icon"
|
||||
class="w-16 h-16 flex-shrink-0"
|
||||
class="w-16 h-16 shrink-0"
|
||||
>
|
||||
<UIcon
|
||||
:name="preview.manifest.icon"
|
||||
@ -184,7 +184,6 @@ const shellPermissions = computed({
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const permissionAccordionItems = computed(() => {
|
||||
const items = []
|
||||
|
||||
|
||||
@ -3,15 +3,17 @@
|
||||
ref="windowEl"
|
||||
:style="windowStyle"
|
||||
:class="[
|
||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden isolate',
|
||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden',
|
||||
'transition-all ease-out duration-600',
|
||||
'flex flex-col @container',
|
||||
{ 'select-none': isResizingOrDragging },
|
||||
isActive ? 'z-20' : 'z-10',
|
||||
// Border colors based on warning level
|
||||
warningLevel === 'warning' ? 'border-2 border-warning-500' :
|
||||
warningLevel === 'danger' ? 'border-2 border-danger-500' :
|
||||
'border border-gray-200 dark:border-gray-700',
|
||||
warningLevel === 'warning'
|
||||
? 'border-2 border-warning-500'
|
||||
: warningLevel === 'danger'
|
||||
? 'border-2 border-danger-500'
|
||||
: 'border border-gray-200 dark:border-gray-700',
|
||||
]"
|
||||
@mousedown="handleActivate"
|
||||
>
|
||||
@ -320,13 +322,33 @@ const handleMaximize = () => {
|
||||
const bounds = getViewportBounds()
|
||||
|
||||
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
||||
// Get safe-area-insets from CSS variables for debug
|
||||
const safeAreaTop = parseFloat(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--safe-area-inset-top',
|
||||
) || '0',
|
||||
)
|
||||
const safeAreaBottom = parseFloat(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--safe-area-inset-bottom',
|
||||
) || '0',
|
||||
)
|
||||
|
||||
// Desktop container uses 'absolute inset-0' which stretches over full viewport
|
||||
// bounds.height = full viewport height (includes header area + safe-areas)
|
||||
// We need to calculate available space properly
|
||||
|
||||
// Get header height from UI store (measured reactively in layout)
|
||||
const uiStore = useUiStore()
|
||||
const headerHeight = uiStore.headerHeight
|
||||
|
||||
x.value = 0
|
||||
y.value = 0
|
||||
y.value = 0 // Start below header and status bar
|
||||
width.value = bounds.width
|
||||
height.value = bounds.height
|
||||
// Height: viewport - header - both safe-areas
|
||||
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
||||
isMaximized.value = true
|
||||
}
|
||||
console.log('handleMaximize', preMaximizeState, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="w-dvw h-dvh flex flex-col">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<UPageHeader
|
||||
ref="headerEl"
|
||||
as="header"
|
||||
:ui="{
|
||||
root: ['px-8 py-0'],
|
||||
@ -53,7 +54,7 @@
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<main class="flex-1 overflow-hidden bg-elevated flex flex-col">
|
||||
<main class="flex-1 overflow-hidden bg-elevated flex flex-col relative">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@ -93,11 +94,9 @@
|
||||
variant="outline"
|
||||
class="mt-6"
|
||||
@click="handleAddWorkspace"
|
||||
icon="i-heroicons-plus"
|
||||
:label="t('add')"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-heroicons-plus" />
|
||||
</template>
|
||||
New Workspace
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -127,6 +126,15 @@ const handleAddWorkspace = async () => {
|
||||
workspaceStore.slideToWorkspace(workspace?.id)
|
||||
})
|
||||
}
|
||||
|
||||
// Measure header height and store it in UI store
|
||||
const headerEl = useTemplateRef('headerEl')
|
||||
const { height } = useElementSize(headerEl)
|
||||
const uiStore = useUiStore()
|
||||
|
||||
watch(height, (newHeight) => {
|
||||
uiStore.headerHeight = newHeight
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
@ -136,10 +144,12 @@ de:
|
||||
|
||||
header:
|
||||
workspaces: Workspaces
|
||||
add: Workspace hinzufügen
|
||||
en:
|
||||
search:
|
||||
label: Search
|
||||
|
||||
header:
|
||||
workspaces: Workspaces
|
||||
add: Add Workspace
|
||||
</i18n>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<UDashboardPanel resizable>
|
||||
<HaexDesktop />
|
||||
</div>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@ -69,10 +69,12 @@ export const useUiStore = defineStore('uiStore', () => {
|
||||
})
|
||||
|
||||
const viewportHeightWithoutHeader = ref(0)
|
||||
const headerHeight = ref(0)
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
viewportHeightWithoutHeader,
|
||||
headerHeight,
|
||||
currentTheme,
|
||||
currentThemeName,
|
||||
defaultTheme,
|
||||
|
||||
Reference in New Issue
Block a user