Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions src/frontend/web/von_interface/static/js/components/dynamicLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Dynamically calculate and set positions for tabs and content based on actual header height
* JVNAUTOSCI-550: Replace hard-coded CSS positions with JavaScript calculation
*/
export function setupDynamicLayout() {
// Reuse the actual diagnostic controls so their handlers and live values survive.
const footerContainer = document.querySelector('.footer-container');
const secondaryFooterItems = footerContainer
? Array.from(footerContainer.children).filter(item => item.id !== 'modelInfoFooter') : [];
const footerDetails = document.createElement('details');
footerDetails.className = 'mobile-footer-details';
const summary = document.createElement('summary');
summary.textContent = 'Info';
summary.setAttribute('aria-label', 'Footer information and diagnostics');
const detailsPanel = document.createElement('div');
detailsPanel.className = 'mobile-footer-details-panel';
footerDetails.append(summary, detailsPanel);

function updateLayout() {
const header = document.getElementById('globalHeader');
const tabContainer = document.querySelector('.tab-container');
const contentAreas = document.querySelectorAll('.tab-content');
const tabContentArea = document.querySelector('.tab-content-area');

if (!header || !tabContainer) {
console.warn('Dynamic layout: Required elements not found');
return;
}

// Get actual header height including search box
const headerHeight = header.getBoundingClientRect().height;
tabContainer.style.top = `${headerHeight}px`;
const measuredTabHeight = Math.max(
44,
Math.ceil(tabContainer.getBoundingClientRect().height || tabContainer.scrollHeight || 44)
);
const tabHeight = measuredTabHeight;
const contentTop = headerHeight + tabHeight + 8; // 8px margin
const narrow = window.matchMedia('(max-width: 800px), (max-width: 1024px) and (pointer: coarse)').matches;
const footer = document.querySelector('.footer-container');
if (footer && narrow && !footerDetails.isConnected) {
detailsPanel.append(...secondaryFooterItems);
footer.append(footerDetails);
} else if (!narrow && footerDetails.isConnected) {
footer.append(...secondaryFooterItems);
footerDetails.remove();
footerDetails.open = false;
}
const footerSpace = narrow ? Math.ceil(footer?.getBoundingClientRect().height || 64) + 8 : Math.max(64, Math.ceil(footer?.getBoundingClientRect().height || 0));
// At normal zoom the visual viewport excludes the on-screen keyboard.
// Pinch zoom must not resize the application or discard a draft.
const viewport = window.visualViewport;
const viewportHeight = narrow && viewport?.scale === 1 ? viewport.height : window.innerHeight;
const keyboardInset = narrow && viewport?.scale === 1
? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop) : 0;
document.documentElement.style.setProperty('--von-viewport-height', `${viewportHeight}px`);
document.documentElement.style.setProperty('--von-keyboard-inset', `${keyboardInset}px`);

document.documentElement.style.setProperty('--von-fixed-shell-height', `${contentTop}px`);
document.documentElement.style.setProperty('--von-fixed-footer-clearance', `${footerSpace}px`);

// Position main tab content area below tabs
if (tabContentArea) {
tabContentArea.style.marginTop = `${contentTop}px`;
// Also update the minimum height calculation to account for dynamic header
const totalTopSpace = contentTop + 20; // content margin + padding
tabContentArea.style.minHeight = `calc(100vh - ${totalTopSpace + footerSpace}px)`;
tabContentArea.style.setProperty('--von-tab-content-available-height', `calc(100vh - ${contentTop + footerSpace}px)`);
tabContentArea.style.removeProperty('height');
tabContentArea.style.overflowY = 'visible';
}

// Position standalone content areas below tabs. Content already inside the
// tab-content-area inherits that offset from the parent and must not stack it.
contentAreas.forEach(area => {
if (tabContentArea && tabContentArea.contains(area)) {
area.style.marginTop = '0px';
} else {
area.style.marginTop = `${contentTop}px`;
}
});
}

// Initial layout
updateLayout();

// Re-calculate on window resize
window.addEventListener('resize', updateLayout);
window.visualViewport?.addEventListener('resize', updateLayout);
window.visualViewport?.addEventListener('scroll', updateLayout);

// Re-calculate when search UI changes (in case it affects header height)
const searchInput = document.getElementById('vontologySearchInput');
// Re-select header here (local inside updateLayout previously) to avoid scope errors
const headerEl = document.getElementById('globalHeader');
const tabContainer = document.querySelector('.tab-container');
if (searchInput && headerEl && !headerEl._dynamicLayoutObserved) {
try {
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(updateLayout);
});
resizeObserver.observe(headerEl);
headerEl._dynamicLayoutObserved = true; // flag to prevent duplicate observers
} catch (e) {
console.warn('Dynamic layout: ResizeObserver setup failed', e);
}
}

if (tabContainer && !tabContainer._dynamicLayoutObserved) {
try {
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(updateLayout);
});
resizeObserver.observe(tabContainer);
tabContainer._dynamicLayoutObserved = true;
} catch (e) {
console.warn('Dynamic layout: Tab container ResizeObserver setup failed', e);
}
}

const footer = document.querySelector('.footer-container');
if (footer && !footer._dynamicLayoutObserved && typeof ResizeObserver === 'function') {
new ResizeObserver(() => requestAnimationFrame(updateLayout)).observe(footer);
footer._dynamicLayoutObserved = true;
}

if (!document._dynamicTabStripLayoutListenerBound) {
document.addEventListener('von:tab-strip-layout-changed', () => {
requestAnimationFrame(updateLayout);
});
document._dynamicTabStripLayoutListenerBound = true;
}
}

1 change: 1 addition & 0 deletions src/frontend/web/von_interface/static/js/domUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,7 @@ export async function setModelInfoFooterText() {
userInfo?.conceptId || null,
userInfo?.name || null,
);
userSegment.classList.add('footer-user-segment');
footerIdentityTargets.push({
button: userSegment.querySelector('.concept-footer-button'),
identityKind: 'User',
Expand Down
103 changes: 1 addition & 102 deletions src/frontend/web/von_interface/static/js/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setupDynamicLayout } from './components/dynamicLayout.js';
import { initializeDomElements, initializeInfoPopup, loadAndDisplayGlobalModelInFooter, setFooterServerReachability } from './domUtils.js';
import {
closeDynamicConceptTab,
Expand Down Expand Up @@ -619,109 +620,7 @@ document.addEventListener('orgSwitched', (event) => {
}
});

/**
* Dynamically calculate and set positions for tabs and content based on actual header height
* JVNAUTOSCI-550: Replace hard-coded CSS positions with JavaScript calculation
*/
function setupDynamicLayout() {
function updateLayout() {
const header = document.getElementById('globalHeader');
const tabContainer = document.querySelector('.tab-container');
const contentAreas = document.querySelectorAll('.tab-content');
const tabContentArea = document.querySelector('.tab-content-area');

if (!header || !tabContainer) {
console.warn('Dynamic layout: Required elements not found');
return;
}

// Get actual header height including search box
const headerHeight = header.getBoundingClientRect().height;
tabContainer.style.top = `${headerHeight}px`;
const measuredTabHeight = Math.max(
44,
Math.ceil(tabContainer.getBoundingClientRect().height || tabContainer.scrollHeight || 44)
);
const tabHeight = measuredTabHeight;
const contentTop = headerHeight + tabHeight + 8; // 8px margin
const footerSpace = Math.max(64, Math.ceil(document.querySelector('.footer-container')?.getBoundingClientRect().height || 0));

document.documentElement.style.setProperty('--von-fixed-shell-height', `${contentTop}px`);
document.documentElement.style.setProperty('--von-fixed-footer-clearance', `${footerSpace}px`);

console.log(`Dynamic layout: Header ${headerHeight}px, positioning tabs at ${headerHeight}px, content at ${contentTop}px`);

// Position main tab content area below tabs
if (tabContentArea) {
tabContentArea.style.marginTop = `${contentTop}px`;
// Also update the minimum height calculation to account for dynamic header
const totalTopSpace = contentTop + 20; // content margin + padding
tabContentArea.style.minHeight = `calc(100vh - ${totalTopSpace + footerSpace}px)`;
tabContentArea.style.setProperty('--von-tab-content-available-height', `calc(100vh - ${contentTop + footerSpace}px)`);
tabContentArea.style.removeProperty('height');
tabContentArea.style.overflowY = 'visible';
}

// Position standalone content areas below tabs. Content already inside the
// tab-content-area inherits that offset from the parent and must not stack it.
contentAreas.forEach(area => {
if (tabContentArea && tabContentArea.contains(area)) {
area.style.marginTop = '0px';
} else {
area.style.marginTop = `${contentTop}px`;
}
});
}

// Initial layout
updateLayout();

// Re-calculate on window resize
window.addEventListener('resize', updateLayout);

// Re-calculate when search UI changes (in case it affects header height)
const searchInput = document.getElementById('vontologySearchInput');
// Re-select header here (local inside updateLayout previously) to avoid scope errors
const headerEl = document.getElementById('globalHeader');
const tabContainer = document.querySelector('.tab-container');
if (searchInput && headerEl && !headerEl._dynamicLayoutObserved) {
try {
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(updateLayout);
});
resizeObserver.observe(headerEl);
headerEl._dynamicLayoutObserved = true; // flag to prevent duplicate observers
} catch (e) {
console.warn('Dynamic layout: ResizeObserver setup failed', e);
}
}

if (tabContainer && !tabContainer._dynamicLayoutObserved) {
try {
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(updateLayout);
});
resizeObserver.observe(tabContainer);
tabContainer._dynamicLayoutObserved = true;
} catch (e) {
console.warn('Dynamic layout: Tab container ResizeObserver setup failed', e);
}
}

const footerEl = document.querySelector('.footer-container');
if (footerEl && !footerEl._dynamicLayoutObserved && typeof ResizeObserver === 'function') {
const footerObserver = new ResizeObserver(() => requestAnimationFrame(updateLayout));
footerObserver.observe(footerEl);
footerEl._dynamicLayoutObserved = true;
}

if (!document._dynamicTabStripLayoutListenerBound) {
document.addEventListener('von:tab-strip-layout-changed', () => {
requestAnimationFrame(updateLayout);
});
document._dynamicTabStripLayoutListenerBound = true;
}
}

function startHealthPolling() {
const localIpSpan = document.getElementById('serverLocalIpValue');
Expand Down
133 changes: 133 additions & 0 deletions src/frontend/web/von_interface/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -18268,3 +18268,136 @@ body.settings-body {
margin-block: 0.3rem;
}
.chat-steering-feedback .btn-mini { flex-shrink: 0; }
/* Phone and folded-device shell. Keep the existing desktop layout and DOM;
resizing must never recreate a conversation or its draft. */
@media (max-width: 800px), (max-width: 1024px) and (pointer: coarse) {
.von-authentication-gate {
min-height: 100dvh;
padding: max(20px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
max(20px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}

.global-header {
padding-top: env(safe-area-inset-top);
}

.global-header-shell {
gap: 8px;
padding: 6px max(8px, env(safe-area-inset-right)) 6px max(8px, env(safe-area-inset-left));
}

.global-header .main-container { flex-basis: 25%; }
.global-header #vontologySearchInput { font-size: 16px; min-height: 44px; }
.tab-container { padding-left: max(8px, env(safe-area-inset-left)); }
.tab-button { min-height: 44px; box-sizing: border-box; }

.tab-content-area {
padding: 0 max(8px, env(safe-area-inset-right)) var(--von-fixed-footer-clearance, 64px)
max(8px, env(safe-area-inset-left));
}

#chatTab > .content-wrapper { width: 100%; }
.chat-session-tabs-row { flex-wrap: wrap; gap: 4px; }
.chat-session-tabs { flex-basis: 100%; }
.chat-session-tab { min-height: 44px; }
.chat-session-history-summary { max-width: 100%; }
.chat-session-history-controls { flex-wrap: wrap; }
.chat-session-history-controls button { min-height: 44px; }
.chat-conversation-masthead { grid-template-columns: minmax(0, 1fr); gap: 4px; }
.chat-header-controls {
flex-wrap: nowrap;
justify-content: flex-start;
overflow-x: auto;
padding-bottom: 4px;
}
.chat-conversation-masthead .chat-export-btn { flex-shrink: 0; min-height: 44px; min-width: 44px; }
.scrollable-field { overflow-wrap: anywhere; }
.scrollable-field pre { max-width: 100%; overflow-x: auto; }
.scrollable-field img { max-width: 100%; height: auto; }

.unified-message-content .message-compose-area {
position: fixed;
left: max(8px, env(safe-area-inset-left));
right: max(8px, env(safe-area-inset-right));
bottom: calc(var(--von-fixed-footer-clearance, 64px) + var(--von-keyboard-inset, 0px));
z-index: 100; padding: 8px; gap: 4px;
max-height: calc(var(--von-viewport-height, 100dvh) - 60px);
overflow-y: auto;
}
.unified-message-content .message-compose-row { flex-direction: row; align-items: flex-end; }
.unified-message-content .message-input {
min-width: 0; min-height: 44px; height: 44px; box-sizing: border-box; font-size: 16px;
}
.unified-message-content .send-message-btn { width: auto; min-height: 44px; }
.unified-message-content .message-view-content { padding-bottom: 130px; }

.chat-composer {
bottom: calc(var(--von-fixed-footer-clearance, 64px) + var(--von-keyboard-inset, 0px));
max-height: calc(var(--von-viewport-height, 100dvh) - 60px);
overflow-y: auto;
}
#promptInput { font-size: 16px; }
.chat-composer-more-actions > summary { min-height: 44px; display: flex; align-items: center; }

.footer-container {
/* Let the fixed organisation menu use the viewport, not a filter-created containing block. */
backdrop-filter: none;
-webkit-backdrop-filter: none;
bottom: var(--von-keyboard-inset, 0px);
padding: 4px max(8px, env(safe-area-inset-right)) max(4px, env(safe-area-inset-bottom))
max(8px, env(safe-area-inset-left));
gap: 8px;
}
.footer-info {
min-width: 0; display: flex; flex-wrap: wrap; gap: 4px;
white-space: normal;
}
.footer-info > .footer-separator { display: none; }
.footer-user-segment { order: -2; }
.footer-org-switcher { order: -1; }
.footer-user-segment, .footer-org-switcher { max-width: 100%; }
.footer-user-segment .concept-footer-button,
.footer-org-current-button { max-width: min(150px, calc((100vw - 140px) / 2)); overflow: hidden; text-overflow: ellipsis; }
.footer-info .conversation-model-controls { flex-basis: 100%; }

.mobile-footer-details { flex-shrink: 0; }
.mobile-footer-details > summary {
min-height: 44px; display: flex; align-items: center; cursor: pointer;
}
.mobile-footer-details-panel {
position: fixed;
left: 8px; right: 8px;
bottom: calc(var(--von-fixed-footer-clearance, 64px) + var(--von-keyboard-inset, 0px));
max-height: calc(var(--von-viewport-height, 100dvh) - var(--von-fixed-footer-clearance, 64px) - 16px);
overflow: auto; padding: 12px;
background: var(--background-color, white); color: var(--text-color, #222);
border: 1px solid #aaa; border-radius: 8px;
}
.mobile-footer-details-panel p { display: block; white-space: normal; }
.footer-info button, .footer-org-menu-trigger { min-height: 44px; }
.footer-org-menu {
position: fixed;
left: max(8px, env(safe-area-inset-left));
right: max(8px, env(safe-area-inset-right));
bottom: calc(var(--von-fixed-footer-clearance, 64px) + var(--von-keyboard-inset, 0px));
width: auto;
max-width: none;
max-height: calc(var(--von-viewport-height, 100dvh) - var(--von-fixed-footer-clearance, 64px) - 16px);
overflow-y: auto;
}

.tab-content.dynamic-concept-tab { width: 100% !important; min-width: 0; }
.concept-header { flex-wrap: wrap; }
.dynamic-concept-tab button, .dynamic-concept-tab input, .dynamic-concept-tab select {
min-height: 44px;
}
.dynamic-concept-tab input, .dynamic-concept-tab select, .dynamic-concept-tab textarea {
font-size: 16px; max-width: 100%; box-sizing: border-box;
}
.dynamic-concept-tab .round-icon-button { min-width: 44px; }
.settings-container { padding: 12px; }
.settings-container select, .settings-container input[type="text"], .settings-container textarea {
max-width: 100%; min-width: 0; box-sizing: border-box; font-size: 16px;
}
.settings-container button, .settings-container select { min-height: 44px; }
}
Loading
Loading