mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +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:
@ -155,13 +155,35 @@ impl ExtensionManager {
|
|||||||
fn extract_and_validate_extension(
|
fn extract_and_validate_extension(
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
temp_prefix: &str,
|
temp_prefix: &str,
|
||||||
|
app_handle: &AppHandle,
|
||||||
) -> Result<ExtractedExtension, ExtensionError> {
|
) -> Result<ExtractedExtension, ExtensionError> {
|
||||||
let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4()));
|
// Use app_cache_dir for better Android compatibility
|
||||||
|
let cache_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_cache_dir()
|
||||||
|
.map_err(|e| ExtensionError::InstallationFailed {
|
||||||
|
reason: format!("Cannot get app cache dir: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let temp_id = uuid::Uuid::new_v4();
|
||||||
|
let temp = cache_dir.join(format!("{}_{}", temp_prefix, temp_id));
|
||||||
|
let zip_file_path = cache_dir.join(format!("{}_{}_{}.haextension", temp_prefix, temp_id, "temp"));
|
||||||
|
|
||||||
|
// Write bytes to a temporary ZIP file first (important for Android file system)
|
||||||
|
fs::write(&zip_file_path, &bytes).map_err(|e| {
|
||||||
|
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Create extraction directory
|
||||||
fs::create_dir_all(&temp)
|
fs::create_dir_all(&temp)
|
||||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
||||||
|
|
||||||
let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| {
|
// Open ZIP file from disk (more reliable on Android than from memory)
|
||||||
|
let zip_file = fs::File::open(&zip_file_path).map_err(|e| {
|
||||||
|
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut archive = ZipArchive::new(zip_file).map_err(|e| {
|
||||||
ExtensionError::InstallationFailed {
|
ExtensionError::InstallationFailed {
|
||||||
reason: format!("Invalid ZIP: {}", e),
|
reason: format!("Invalid ZIP: {}", e),
|
||||||
}
|
}
|
||||||
@ -173,6 +195,9 @@ impl ExtensionManager {
|
|||||||
reason: format!("Cannot extract ZIP: {}", e),
|
reason: format!("Cannot extract ZIP: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Clean up temporary ZIP file
|
||||||
|
let _ = fs::remove_file(&zip_file_path);
|
||||||
|
|
||||||
// Read haextension_dir from config if it exists, otherwise use default
|
// Read haextension_dir from config if it exists, otherwise use default
|
||||||
let config_path = temp.join("haextension.config.json");
|
let config_path = temp.join("haextension.config.json");
|
||||||
let haextension_dir = if config_path.exists() {
|
let haextension_dir = if config_path.exists() {
|
||||||
@ -491,9 +516,10 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
pub async fn preview_extension_internal(
|
pub async fn preview_extension_internal(
|
||||||
&self,
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?;
|
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
|
||||||
|
|
||||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||||
&extracted.manifest.public_key,
|
&extracted.manifest.public_key,
|
||||||
@ -518,7 +544,7 @@ impl ExtensionManager {
|
|||||||
custom_permissions: EditablePermissions,
|
custom_permissions: EditablePermissions,
|
||||||
state: &State<'_, AppState>,
|
state: &State<'_, AppState>,
|
||||||
) -> Result<String, ExtensionError> {
|
) -> Result<String, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?;
|
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
|
||||||
|
|
||||||
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
||||||
ExtensionCrypto::verify_signature(
|
ExtensionCrypto::verify_signature(
|
||||||
|
|||||||
@ -48,7 +48,9 @@ impl ExtensionCrypto {
|
|||||||
let relative = path.strip_prefix(dir)
|
let relative = path.strip_prefix(dir)
|
||||||
.unwrap_or(&path)
|
.unwrap_or(&path)
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string()
|
||||||
|
// Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz
|
||||||
|
.replace('\\', "/");
|
||||||
(relative, path)
|
(relative, path)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -56,16 +58,30 @@ impl ExtensionCrypto {
|
|||||||
// 3. Sortiere nach relativen Pfaden
|
// 3. Sortiere nach relativen Pfaden
|
||||||
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
println!("=== Files to hash ({}): ===", relative_files.len());
|
|
||||||
for (rel, _) in &relative_files {
|
|
||||||
println!(" - {}", rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
|
|
||||||
|
// Canonicalize manifest path for comparison (important on Android where symlinks may differ)
|
||||||
|
// Also ensure the canonical path is still within the allowed directory (security check)
|
||||||
|
let canonical_manifest_path = manifest_path.canonicalize()
|
||||||
|
.unwrap_or_else(|_| manifest_path.to_path_buf());
|
||||||
|
|
||||||
|
// Security: Verify canonical manifest path is still within dir
|
||||||
|
let canonical_dir = dir.canonicalize()
|
||||||
|
.unwrap_or_else(|_| dir.to_path_buf());
|
||||||
|
|
||||||
|
if !canonical_manifest_path.starts_with(&canonical_dir) {
|
||||||
|
return Err(ExtensionError::ManifestError {
|
||||||
|
reason: format!("Manifest path resolves outside of extension directory (potential path traversal)"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Inhalte der sortierten Dateien hashen
|
// 4. Inhalte der sortierten Dateien hashen
|
||||||
for (_relative, file_path) in relative_files {
|
for (_relative, file_path) in relative_files {
|
||||||
if file_path == manifest_path {
|
// Canonicalize file_path for comparison
|
||||||
|
let canonical_file_path = file_path.canonicalize()
|
||||||
|
.unwrap_or_else(|_| file_path.clone());
|
||||||
|
|
||||||
|
if canonical_file_path == canonical_manifest_path {
|
||||||
// FÜR DIE MANIFEST.JSON:
|
// FÜR DIE MANIFEST.JSON:
|
||||||
let content_str = fs::read_to_string(&file_path)
|
let content_str = fs::read_to_string(&file_path)
|
||||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
@ -94,8 +110,12 @@ impl ExtensionCrypto {
|
|||||||
reason: format!("Failed to serialize manifest: {}", e),
|
reason: format!("Failed to serialize manifest: {}", e),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
println!("canonical_manifest_content: {}", canonical_manifest_content);
|
|
||||||
hasher.update(canonical_manifest_content.as_bytes());
|
// Normalisiere Zeilenenden zu Unix-Style (\n), wie Node.js JSON.stringify es macht
|
||||||
|
// Dies ist wichtig für plattformübergreifende Konsistenz (Desktop vs Android)
|
||||||
|
let normalized_content = canonical_manifest_content.replace("\r\n", "\n");
|
||||||
|
|
||||||
|
hasher.update(normalized_content.as_bytes());
|
||||||
} else {
|
} else {
|
||||||
// FÜR ALLE ANDEREN DATEIEN:
|
// FÜR ALLE ANDEREN DATEIEN:
|
||||||
let content =
|
let content =
|
||||||
|
|||||||
@ -82,12 +82,13 @@ pub async fn get_all_extensions(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn preview_extension(
|
pub async fn preview_extension(
|
||||||
|
app_handle: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
state
|
state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
.preview_extension_internal(file_bytes)
|
.preview_extension_internal(&app_handle, file_bytes)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,18 +14,44 @@
|
|||||||
@apply cursor-not-allowed;
|
@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 {
|
#__nuxt {
|
||||||
/* Stellt sicher, dass die App immer die volle Höhe hat */
|
/* Volle Höhe des body */
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
/* Sorgt dafür, dass Padding die Höhe nicht sprengt */
|
/* Safe-Area Paddings auf root element - damit ALLES davon profitiert */
|
||||||
@apply box-border;
|
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 */
|
box-sizing: border-box;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="desktopEl"
|
ref="desktopEl"
|
||||||
class="w-full h-full relative overflow-hidden"
|
class="absolute inset-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<Swiper
|
<Swiper
|
||||||
:modules="[SwiperNavigation]"
|
:modules="[SwiperNavigation]"
|
||||||
@ -13,7 +13,7 @@
|
|||||||
:no-swiping="true"
|
:no-swiping="true"
|
||||||
no-swiping-class="no-swipe"
|
no-swiping-class="no-swipe"
|
||||||
:allow-touch-move="allowSwipe"
|
:allow-touch-move="allowSwipe"
|
||||||
class="w-full h-full"
|
class="h-full w-full"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
@swiper="onSwiperInit"
|
@swiper="onSwiperInit"
|
||||||
@slide-change="onSlideChange"
|
@slide-change="onSlideChange"
|
||||||
@ -115,7 +115,14 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
: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"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
@ -165,7 +172,13 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
: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"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
v-if="preview?.manifest.icon"
|
v-if="preview?.manifest.icon"
|
||||||
class="w-16 h-16 flex-shrink-0"
|
class="w-16 h-16 shrink-0"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
:name="preview.manifest.icon"
|
:name="preview.manifest.icon"
|
||||||
@ -184,7 +184,6 @@ const shellPermissions = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const permissionAccordionItems = computed(() => {
|
const permissionAccordionItems = computed(() => {
|
||||||
const items = []
|
const items = []
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,17 @@
|
|||||||
ref="windowEl"
|
ref="windowEl"
|
||||||
:style="windowStyle"
|
:style="windowStyle"
|
||||||
:class="[
|
: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',
|
'transition-all ease-out duration-600',
|
||||||
'flex flex-col @container',
|
'flex flex-col @container',
|
||||||
{ 'select-none': isResizingOrDragging },
|
{ 'select-none': isResizingOrDragging },
|
||||||
isActive ? 'z-20' : 'z-10',
|
isActive ? 'z-20' : 'z-10',
|
||||||
// Border colors based on warning level
|
// Border colors based on warning level
|
||||||
warningLevel === 'warning' ? 'border-2 border-warning-500' :
|
warningLevel === 'warning'
|
||||||
warningLevel === 'danger' ? 'border-2 border-danger-500' :
|
? 'border-2 border-warning-500'
|
||||||
'border border-gray-200 dark:border-gray-700',
|
: warningLevel === 'danger'
|
||||||
|
? 'border-2 border-danger-500'
|
||||||
|
: 'border border-gray-200 dark:border-gray-700',
|
||||||
]"
|
]"
|
||||||
@mousedown="handleActivate"
|
@mousedown="handleActivate"
|
||||||
>
|
>
|
||||||
@ -320,13 +322,33 @@ const handleMaximize = () => {
|
|||||||
const bounds = getViewportBounds()
|
const bounds = getViewportBounds()
|
||||||
|
|
||||||
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
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
|
x.value = 0
|
||||||
y.value = 0
|
y.value = 0 // Start below header and status bar
|
||||||
width.value = bounds.width
|
width.value = bounds.width
|
||||||
height.value = bounds.height
|
// Height: viewport - header - both safe-areas
|
||||||
|
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
||||||
isMaximized.value = true
|
isMaximized.value = true
|
||||||
}
|
}
|
||||||
console.log('handleMaximize', preMaximizeState, bounds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-dvw h-dvh flex flex-col">
|
<div class="w-full h-full flex flex-col">
|
||||||
<UPageHeader
|
<UPageHeader
|
||||||
|
ref="headerEl"
|
||||||
as="header"
|
as="header"
|
||||||
:ui="{
|
:ui="{
|
||||||
root: ['px-8 py-0'],
|
root: ['px-8 py-0'],
|
||||||
@ -53,7 +54,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</UPageHeader>
|
</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 />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -93,11 +94,9 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="mt-6"
|
class="mt-6"
|
||||||
@click="handleAddWorkspace"
|
@click="handleAddWorkspace"
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
:label="t('add')"
|
||||||
>
|
>
|
||||||
<template #leading>
|
|
||||||
<UIcon name="i-heroicons-plus" />
|
|
||||||
</template>
|
|
||||||
New Workspace
|
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -127,6 +126,15 @@ const handleAddWorkspace = async () => {
|
|||||||
workspaceStore.slideToWorkspace(workspace?.id)
|
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>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
@ -136,10 +144,12 @@ de:
|
|||||||
|
|
||||||
header:
|
header:
|
||||||
workspaces: Workspaces
|
workspaces: Workspaces
|
||||||
|
add: Workspace hinzufügen
|
||||||
en:
|
en:
|
||||||
search:
|
search:
|
||||||
label: Search
|
label: Search
|
||||||
|
|
||||||
header:
|
header:
|
||||||
workspaces: Workspaces
|
workspaces: Workspaces
|
||||||
|
add: Add Workspace
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex items-center justify-center">
|
<UDashboardPanel resizable>
|
||||||
<HaexDesktop />
|
<HaexDesktop />
|
||||||
</div>
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@ -69,10 +69,12 @@ export const useUiStore = defineStore('uiStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const viewportHeightWithoutHeader = ref(0)
|
const viewportHeightWithoutHeader = ref(0)
|
||||||
|
const headerHeight = ref(0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableThemes,
|
availableThemes,
|
||||||
viewportHeightWithoutHeader,
|
viewportHeightWithoutHeader,
|
||||||
|
headerHeight,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
currentThemeName,
|
currentThemeName,
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
|
|||||||
Reference in New Issue
Block a user