- Wrap client in @vueuse/core to abstract mobile client dimensions

This commit is contained in:
Josako
2025-11-26 08:01:33 +01:00
parent 73125887a3
commit 0d3c3949de
9 changed files with 298 additions and 30 deletions

View File

@@ -1,6 +1,6 @@
{ {
"dist/chat-client.js": "dist/chat-client.f7134231.js", "dist/chat-client.js": "dist/chat-client.f8ee4d5a.js",
"dist/chat-client.css": "dist/chat-client.99e10656.css", "dist/chat-client.css": "dist/chat-client.2fffefae.css",
"dist/main.js": "dist/main.6a617099.js", "dist/main.js": "dist/main.6a617099.js",
"dist/main.css": "dist/main.7182aac3.css" "dist/main.css": "dist/main.7182aac3.css"
} }

View File

@@ -9,6 +9,13 @@
--message-bot-bg: #f8f9fa; --message-bot-bg: #f8f9fa;
--border-radius: 8px; --border-radius: 8px;
--spacing: 16px; --spacing: 16px;
/* Nieuwe, veilige viewport-variabelen voor mobiele lay-out
- --vvh blijft als fallback uit viewport.js
- --safe-vh wordt gezet door useChatViewport()
- --safe-bottom-inset wordt gebruikt voor ondermarge bij de chat input */
--safe-vh: var(--vvh, 1vh);
--safe-bottom-inset: 0px;
} }
/* App container layout */ /* App container layout */
@@ -16,7 +23,7 @@
display: flex; display: flex;
/* Use visual viewport variable when available */ /* Use visual viewport variable when available */
min-height: 0; min-height: 0;
height: calc(var(--vvh, 1vh) * 100); height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
width: 100%; width: 100%;
} }
@@ -97,7 +104,8 @@
} }
html, body { html, body {
height: calc(var(--vvh, 1vh) * 100); min-height: 0; height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
min-height: 0;
} }
/* Base reset & overflow control */ /* Base reset & overflow control */

View File

@@ -0,0 +1,176 @@
// useChatViewport.js
// Centrale viewport-/keyboard-laag voor de chat client.
// - Gebruikt visualViewport + VueUse (via window.VueUse) om veilige hoogte
// en keyboard-status te bepalen
// - Stelt CSS-variabelen --safe-vh en --safe-bottom-inset in op <html>
// - Beheert body-klassen zoals chat-keyboard-open en ios-safari
import { ref, computed, watchEffect, onMounted, onBeforeUnmount } from 'vue';
// Haal VueUse-composables uit window.VueUse, die in de chat-bundel
// worden geïnitialiseerd via nginx/frontend_src/js/vueuse-setup.js.
// Als ze ontbreken (bv. in een testsituatie), vallen we terug op
// eenvoudige lokale shims.
const vueUse = (typeof window !== 'undefined' && window.VueUse) || {};
let { useEventListener, useWindowSize } = vueUse;
// Fallback-shim voor useEventListener
if (!useEventListener) {
useEventListener = (target, event, handler, options) => {
if (!target || !target.addEventListener) return () => {};
const opts = options || { passive: true };
target.addEventListener(event, handler, opts);
return () => target.removeEventListener(event, handler, opts);
};
}
// Fallback-shim voor useWindowSize
if (!useWindowSize) {
useWindowSize = () => {
const height = ref(typeof window !== 'undefined' ? window.innerHeight : 0);
if (typeof window !== 'undefined') {
window.addEventListener('resize', () => {
height.value = window.innerHeight;
}, { passive: true });
}
return { height };
};
}
function detectIosSafari() {
if (typeof navigator === 'undefined') return false;
const ua = navigator.userAgent || navigator.vendor || (window && window.opera) || '';
const isIOS = /iP(ad|hone|od)/.test(ua);
const isSafari = /Safari/.test(ua) && !/Chrome|CriOS|FxiOS/.test(ua);
return isIOS && isSafari;
}
// Drempel voor het detecteren van keyboard-open op basis van hoogteverschil
const KEYBOARD_DELTA_THRESHOLD = 120; // px, afgestemd op bestaande viewport.js heuristiek
export function useChatViewport() {
const isClient = typeof window !== 'undefined' && typeof document !== 'undefined';
const safeHeight = ref(isClient ? window.innerHeight : 0);
const baselineHeight = ref(null); // baseline voor keyboard-detectie
const keyboardOpen = ref(false);
const safeBottomInset = ref(0);
const { height: windowHeight } = useWindowSize();
const isIosSafari = ref(isClient ? detectIosSafari() : false);
const isMobile = computed(() => {
if (!isClient) return false;
// Houd de definitie in lijn met bestaande CSS (max-width: 768px)
return window.innerWidth <= 768;
});
function applyToCss() {
if (!isClient) return;
const root = document.documentElement;
const vhUnit = (safeHeight.value || windowHeight.value || 0) / 100;
if (vhUnit > 0) {
root.style.setProperty('--safe-vh', `${vhUnit}px`);
}
root.style.setProperty('--safe-bottom-inset', `${safeBottomInset.value || 0}px`);
// Body-klassen
document.body.classList.toggle('chat-keyboard-open', !!keyboardOpen.value);
if (isIosSafari.value) {
root.classList.add('ios-safari');
} else {
root.classList.remove('ios-safari');
}
}
function updateFromVisualViewport() {
if (!isClient) return;
const vv = window.visualViewport;
const currentHeight = vv ? vv.height : window.innerHeight;
safeHeight.value = currentHeight;
// Bepaal een eenvoudige bottom-inset: verschil tussen layout- en visual viewport
const layoutHeight = window.innerHeight || currentHeight;
const inset = Math.max(0, layoutHeight - currentHeight);
safeBottomInset.value = inset;
// Keyboard-detectie: vergelijk met baseline
if (baselineHeight.value == null) {
baselineHeight.value = currentHeight;
}
const delta = (baselineHeight.value || currentHeight) - currentHeight;
keyboardOpen.value = delta > KEYBOARD_DELTA_THRESHOLD;
}
onMounted(() => {
if (!isClient) return;
// Initiale meting
updateFromVisualViewport();
applyToCss();
const vv = window.visualViewport;
// Luister naar relevante events via VueUse
const stopResize = vv
? useEventListener(vv, 'resize', () => {
updateFromVisualViewport();
applyToCss();
})
: useEventListener(window, 'resize', () => {
updateFromVisualViewport();
applyToCss();
});
const stopScroll = vv
? useEventListener(vv, 'scroll', () => {
updateFromVisualViewport();
applyToCss();
})
: () => {};
const stopOrientation = useEventListener(window, 'orientationchange', () => {
// Bij oriëntatie-wijziging baseline resetten
baselineHeight.value = null;
updateFromVisualViewport();
applyToCss();
});
onBeforeUnmount(() => {
stopResize && stopResize();
stopScroll && stopScroll();
stopOrientation && stopOrientation();
});
});
// Reageer ook op generieke windowHeight veranderingen (fallback)
watchEffect(() => {
if (!isClient) return;
if (!window.visualViewport) {
safeHeight.value = windowHeight.value;
safeBottomInset.value = 0;
keyboardOpen.value = false;
applyToCss();
}
});
// Initiale toepassing voor SSR / eerste render
if (isClient) {
applyToCss();
}
return {
safeHeight,
safeBottomInset,
keyboardOpen,
isMobile,
isIosSafari,
};
}
export default useChatViewport;

View File

@@ -0,0 +1,13 @@
<template>
<SafeViewport>
<ChatApp />
</SafeViewport>
</template>
<script setup>
// ChatRoot.vue
// Kleine root-component die de ChatApp binnen de SafeViewport wrapper rendert.
import ChatApp from './ChatApp.vue';
import SafeViewport from './SafeViewport.vue';
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="safe-viewport-wrapper">
<slot />
</div>
</template>
<script setup>
// SafeViewport.vue
// Wrapper-component die de chatapplicatie omhult en de viewport-/keyboard-logica
// initialiseert via de useChatViewport composable.
// Belangrijk: de composable zelf leeft onder nginx/frontend_src/js zodat hij
// binnen dezelfde npm-package valt als de bundel (en zo @vueuse/core kan resolven).
// Daarom gebruiken we hier een relatieve pad vanuit de Vue-component naar die map.
import useChatViewport from '../js/composables/useChatViewport.js';
// Initialiseer de viewport-logica zodra deze wrapper instantiëert.
useChatViewport();
</script>
<style scoped>
.safe-viewport-wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -10,7 +10,6 @@ import '../../../eveai_chat_client/static/assets/css/form-message.css';
// Dependencies // Dependencies
import { createApp, version } from 'vue'; import { createApp, version } from 'vue';
import { marked } from 'marked'; import { marked } from 'marked';
import { FormField } from '../../../../../../../../../Users/josako/Library/Application Support/JetBrains/PyCharm2025.1/scratches/old js files/FormField.js';
// Import LanguageProvider for sidebar translation support // Import LanguageProvider for sidebar translation support
import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../../../eveai_chat_client/static/assets/js/services/LanguageProvider.js'; import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../../../eveai_chat_client/static/assets/js/services/LanguageProvider.js';
@@ -29,9 +28,13 @@ console.log('Components loaded:', Object.keys(Components));
// Import specifieke componenten // Import specifieke componenten
import LanguageSelector from '../../../eveai_chat_client/static/assets/vue-components/LanguageSelector.vue'; import LanguageSelector from '../../../eveai_chat_client/static/assets/vue-components/LanguageSelector.vue';
import ChatApp from '../../../eveai_chat_client/static/assets/vue-components/ChatApp.vue'; import ChatApp from '../../../eveai_chat_client/static/assets/vue-components/ChatApp.vue';
import ChatRoot from '../../../eveai_chat_client/static/assets/vue-components/ChatRoot.vue';
import SideBar from '../../../eveai_chat_client/static/assets/vue-components/SideBar.vue'; import SideBar from '../../../eveai_chat_client/static/assets/vue-components/SideBar.vue';
import MobileHeader from '../../../eveai_chat_client/static/assets/vue-components/MobileHeader.vue'; import MobileHeader from '../../../eveai_chat_client/static/assets/vue-components/MobileHeader.vue';
// VueUse-setup voor de chatclient (maakt composables beschikbaar via window.VueUse)
import './vueuse-setup.js';
// Globale Vue error tracking // Globale Vue error tracking
window.addEventListener('error', function(event) { window.addEventListener('error', function(event) {
console.error('🚨 [Global Error]', event.error); console.error('🚨 [Global Error]', event.error);
@@ -230,8 +233,8 @@ function initializeChatApp() {
allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'] allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de']
}; };
// Mount de component met alle nodige componenten // Mount de component via ChatRoot zodat SafeViewport de layout kan beheren
const app = createApp(ChatApp, props); const app = createApp(ChatRoot, props);
// SSE verbinding configuratie - injecteren in ChatApp component // SSE verbinding configuratie - injecteren in ChatApp component
app.provide('sseConfig', { app.provide('sseConfig', {

View File

@@ -0,0 +1,15 @@
// vueuse-setup.js
// Setup-module voor VueUse in de chat client.
// - Importeert benodigde composables uit '@vueuse/core'
// - Maakt ze beschikbaar via window.VueUse zodat static assets
// onder eveai_chat_client/static ze kunnen gebruiken zonder
// directe npm-imports.
import { useEventListener, useWindowSize } from '@vueuse/core';
if (typeof window !== 'undefined') {
window.VueUse = Object.assign({}, window.VueUse, {
useEventListener,
useWindowSize,
});
}

View File

@@ -6,12 +6,13 @@
"": { "": {
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@vueuse/core": "^14.0.0",
"animejs": "^4.0.2", "animejs": "^4.0.2",
"bootstrap": "^5.3.6", "bootstrap": "^5.3.6",
"datatables.net": "^2.3.1", "datatables.net": "^2.3.1",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"marked": "^16.0.0", "marked": "16.3.0",
"nouislider": "^15.8.1", "nouislider": "^15.8.1",
"parallax": "^0.0.0", "parallax": "^0.0.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
@@ -22,7 +23,6 @@
"vue": "^3.5.17" "vue": "^3.5.17"
}, },
"devDependencies": { "devDependencies": {
"@parcel/reporter-bundle-analyzer": "^2.15.2",
"@parcel/transformer-sass": "^2.15.2", "@parcel/transformer-sass": "^2.15.2",
"@parcel/transformer-vue": "^2.15.2", "@parcel/transformer-vue": "^2.15.2",
"parcel": "^2.15.2" "parcel": "^2.15.2"
@@ -1360,26 +1360,6 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/@parcel/reporter-bundle-analyzer": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/@parcel/reporter-bundle-analyzer/-/reporter-bundle-analyzer-2.16.0.tgz",
"integrity": "sha512-81EazkM7YjeFvzRRlKKV8kmLfLXfWlfLqlpC8jFRXthC43aZtdqMnzRgi9Q+6FIB7Ft3vnMXZPqRs4cn7+XgUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@parcel/plugin": "2.16.0",
"@parcel/utils": "2.16.0",
"nullthrows": "^1.1.1"
},
"engines": {
"node": ">= 16.0.0",
"parcel": "^2.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/reporter-cli": { "node_modules/@parcel/reporter-cli": {
"version": "2.16.0", "version": "2.16.0",
"resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.16.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.16.0.tgz",
@@ -2722,6 +2702,12 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.21", "version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
@@ -2822,6 +2808,44 @@
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vueuse/core": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0.tgz",
"integrity": "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.0.0",
"@vueuse/shared": "14.0.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0.tgz",
"integrity": "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.0.0.tgz",
"integrity": "sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",

View File

@@ -14,7 +14,8 @@
"tabulator-tables": "^6.3.1", "tabulator-tables": "^6.3.1",
"typed.js": "^2.1.0", "typed.js": "^2.1.0",
"vanilla-jsoneditor": "^3.5.0", "vanilla-jsoneditor": "^3.5.0",
"vue": "^3.5.17" "vue": "^3.5.17",
"@vueuse/core": "^14.0.0"
}, },
"devDependencies": { "devDependencies": {
"@parcel/transformer-sass": "^2.15.2", "@parcel/transformer-sass": "^2.15.2",