diff --git a/doc/changelog.d/109.fixed.md b/doc/changelog.d/109.fixed.md
new file mode 100644
index 00000000..a8a06727
--- /dev/null
+++ b/doc/changelog.d/109.fixed.md
@@ -0,0 +1 @@
+Visor dash styling
diff --git a/src/ansys/visor/dash/src/lib/components/Visordash.react.js b/src/ansys/visor/dash/src/lib/components/Visordash.react.js
index c9b0e073..a6f68b69 100644
--- a/src/ansys/visor/dash/src/lib/components/Visordash.react.js
+++ b/src/ansys/visor/dash/src/lib/components/Visordash.react.js
@@ -96,7 +96,7 @@ const Visordash = (props) => {
}, [fireSnapshot]);
return (
-
+
);
diff --git a/src/ansys/visor/visor-client/index.html b/src/ansys/visor/visor-client/index.html
index c4655b80..283475de 100644
--- a/src/ansys/visor/visor-client/index.html
+++ b/src/ansys/visor/visor-client/index.html
@@ -1,5 +1,5 @@
-
+
@@ -9,6 +9,7 @@
diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx
index 30e873fb..d59d5131 100644
--- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx
+++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx
@@ -2,6 +2,7 @@ import { FC, RefObject, useEffect, useRef } from 'react';
import { AwcIcons } from '../AwcIcons';
import { VisorFrontend } from '../../VisorFrontend.tsx';
import { makeTooltip } from '../../utils/Tooltip';
+import { appendToPortal, removeFromPortal } from '../../utils/portal';
export type Panel_BottomMiddle_Util = {};
@@ -112,7 +113,7 @@ export const Panel_BottomMiddle: FC<{
z-index: 1000;
white-space: nowrap;
`;
- document.body.appendChild(popup);
+ appendToPortal(popup);
const subModes: SelectionMode[] = ['edge', 'face', 'vertex'];
const popupCleanupFuncs: Array<() => void> = [];
@@ -180,7 +181,7 @@ export const Panel_BottomMiddle: FC<{
cleanupFuncs.push(...popupCleanupFuncs);
cleanupFuncs.push(() => document.removeEventListener('click', closePopupOnOutsideClick));
- cleanupFuncs.push(() => popup.remove());
+ cleanupFuncs.push(() => removeFromPortal(popup));
//////////////////////////
onLoad({});
//////////////////////////
diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx
index 5f3bf8a8..4d4a1a94 100644
--- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx
+++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx
@@ -45,7 +45,11 @@ export const Panel_TopLeft: FC<{
await visorState.render();
})();
});
- const fontSize: number = getRootFontSize(14);
+ const fontSize: number = getRootFontSize(
+ 14,
+ (reactComponentContainer.closest('.visor-embed-style') ??
+ document.documentElement) as HTMLElement
+ );
const collapseButton = AwcIcons.getArrowHeadDownIcon(fontSize * 1.2, 3, true, [
'theme-hover-background-3',
]) as HTMLButtonElement;
diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx
index 631d4647..42f45b92 100644
--- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx
+++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx
@@ -218,7 +218,11 @@ export const Panel_TopRight: FC<{
legendPanelApplyRangeButtonId
) as HTMLButtonElement;
- const fontSize: number = getRootFontSize(14);
+ const fontSize: number = getRootFontSize(
+ 14,
+ (componentContainer.closest('.visor-embed-style') ??
+ document.documentElement) as HTMLElement
+ );
let collapsePanel: () => void;
let expandPanel: () => void;
diff --git a/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js b/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js
index 305e41a0..3800c057 100644
--- a/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js
+++ b/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js
@@ -17,8 +17,9 @@ import {
} from '../utils/JsHelpers.js';
/**
- * getRootFontSize caches module-level state, so each test needs a fresh
- * instance of the module.
+ * getRootFontSize caches per-element state at module scope, so each test
+ * needs a fresh instance of the module to avoid leaking cached values
+ * between tests.
*
* @returns {Promise<(fallback: number) => number>}
*/
@@ -126,6 +127,24 @@ describe('getRootFontSize', () => {
expect(getRootFontSize(20)).toBe(20);
expect(getComputedStyleSpy).toHaveBeenCalledTimes(1);
});
+
+ test('caches independently per element, regardless of call order', async () => {
+ const getRootFontSize = await loadFreshGetRootFontSize();
+ const htmlElem = document.documentElement;
+ const embedRootElem = document.createElement('div');
+
+ jest.spyOn(globalThis, 'getComputedStyle').mockImplementation((element) => ({
+ getPropertyValue: () => (element === embedRootElem ? '14px' : '16px'),
+ }));
+
+ // The host resolves first (e.g. a panel without an embed root yet)...
+ expect(getRootFontSize(16, htmlElem)).toBe(16);
+ // ...but a later call with the embed root must not reuse that cached value.
+ expect(getRootFontSize(14, embedRootElem)).toBe(14);
+ // Subsequent calls for each element still return their own cached value.
+ expect(getRootFontSize(16, htmlElem)).toBe(16);
+ expect(getRootFontSize(14, embedRootElem)).toBe(14);
+ });
});
describe('hashString', () => {
diff --git a/src/ansys/visor/visor-client/src/styles/index.css b/src/ansys/visor/visor-client/src/styles/index.css
index aad782cd..e2a58177 100644
--- a/src/ansys/visor/visor-client/src/styles/index.css
+++ b/src/ansys/visor/visor-client/src/styles/index.css
@@ -1,6 +1,10 @@
-*,
-*::before,
-*::after {
+/* Scope the global reset to `.visor-embed-style` so it does not
+ affect the host application when Visor is embedded. Wrap Visor content
+ in a container with class `visor-embed-style`. */
+.visor-embed-style,
+.visor-embed-style *,
+.visor-embed-style *::before,
+.visor-embed-style *::after {
margin: 0;
padding: 0;
line-height: 1;
@@ -12,27 +16,31 @@
visibility: inherit;
}
-html {
+html,
+body {
+ /* Baseline reset kept global (not scoped): a non-zero default body
+ margin causes a viewport scrollbar and a resized/clipped full-page
+ canvas in standalone/Dash-hosted pages. This is standard boilerplate
+ (unlike font-family/line-height) and does not visibly affect a host
+ app's existing content when Visor is truly embedded as a widget. */
height: 100%;
+ margin: 0;
}
@font-face {
/*
The "Source Sans 3" font is used by the Ansys AWC React library,
- and was imported manually from there for use in Visor.
+ and is served by the Visor backend for embedded usage.
*/
font-family: 'Source Sans 3';
src: url('/fonts/source-sans-3.woff2') format('woff2');
+ font-display: swap;
}
-body {
+/* Scoped styles: set height and the AWC font only within the Visor embed root
+ so these font and sizing rules do not leak into host applications. */
+.visor-embed-style {
height: 100%;
- /*
- This font list is used by the Ansys AWC React library,
- and was imported manually from there for use in Visor.
- This font list can be found in CSS file "@ansys\awc-react\styles\themes\css\default.css"
- after installing the AWC React npm package.
- */
font-family:
'Source Sans 3',
-apple-system,
@@ -50,6 +58,15 @@ body {
'Noto Color Emoji';
}
+/* Ensure Visor typography matches standalone appearance while keeping
+ styles scoped to the embed root container so they don't leak into host apps. */
+.visor-embed-style,
+.visor-embed-style *,
+.visor-embed-style * {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
a,
a:active,
a:link,
diff --git a/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css
index 911acc5a..6067617c 100644
--- a/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css
+++ b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css
@@ -4,8 +4,11 @@
border-color: rgba(var(--theme-color-borders), 1);
}
-html,
-body {
+/* Theme scoping: apply these base rules only inside `.visor-embed-style` to avoid
+ leaking layout and color changes into the host application when Visor is
+ embedded as a component. Wrap the Visor content in a container with class
+ `visor-embed-style`. */
+.visor-embed-style {
font-size: 14px;
color: rgba(var(--theme-color-text), 1);
}
diff --git a/src/ansys/visor/visor-client/src/treeview/TreeView.tsx b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx
index fa797b47..067649ca 100644
--- a/src/ansys/visor/visor-client/src/treeview/TreeView.tsx
+++ b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx
@@ -160,10 +160,17 @@ export const TreeView =
>(props: TreeViewProps) =>
if (sceneGraph == null) {
return;
}
- const fontSize: number = getRootFontSize(14);
const componentContainerElem = document.getElementById(
componentContainerElemId
) as HTMLDivElement;
+ // Read the base font size from the nearest `.visor-embed-style` root
+ // rather than `document.documentElement`: when embedded, only the
+ // Visor root (not the host page's ) is scoped to Visor's font size.
+ const embedRoot = componentContainerElem.closest('.visor-embed-style');
+ const fontSize: number = getRootFontSize(
+ 14,
+ (embedRoot ?? document.documentElement) as HTMLElement
+ );
const headerContainer = document.createElement('div');
const treeContainer = document.createElement('div');
componentContainerElem.appendChild(headerContainer);
diff --git a/src/ansys/visor/visor-client/src/utils/JsHelpers.js b/src/ansys/visor/visor-client/src/utils/JsHelpers.js
index 9b0c3a44..a0faefe7 100644
--- a/src/ansys/visor/visor-client/src/utils/JsHelpers.js
+++ b/src/ansys/visor/visor-client/src/utils/JsHelpers.js
@@ -1,34 +1,49 @@
-/** @type {number|null} */
-let rootFontSize = null;
+/**
+ * Per-element cache for computed root font sizes. Keyed by element (rather
+ * than a single module-level value) so callers passing different elements
+ * (e.g. `document.documentElement` vs. a `.visor-embed-style` root) don't
+ * clobber each other's cached result regardless of call order.
+ * @type {WeakMap}
+ */
+const rootFontSizeCache = new WeakMap();
/**
- * Returns the root element's computed font size in pixels.
+ * Returns the given element's computed font size in pixels.
*
* If the computed font size is not expressed in pixels, the supplied fallback
* value is returned. The computed value, or the failure to obtain one, is
- * cached for subsequent calls.
+ * cached per-element for subsequent calls.
*
* @param {number} fallback - Value to return when the root font size cannot be
* determined in pixels. Must be zero or greater.
+ * @param {Element} [element] - Element whose computed font size is read.
+ * Defaults to `document.documentElement`. Pass the `.visor-embed-style` root
+ * when available, since that is where Visor's base font size is scoped to
+ * (the host page's `` element is not modified when embedded).
* @returns {number} The root font size in pixels, or the fallback value.
* @throws {Error} If `fallback` is not a non-negative number.
*/
-export const getRootFontSize = (fallback) => {
+export const getRootFontSize = (fallback, element = document.documentElement) => {
if (typeof fallback !== 'number' || fallback < 0) {
throw new Error('fallback must be a number 0 or greater');
}
+ let rootFontSize = rootFontSizeCache.get(element);
+
if (rootFontSize == null) {
- const str = getComputedStyle(document.documentElement, null).getPropertyValue('font-size');
+ const str = getComputedStyle(element, null).getPropertyValue('font-size');
if (/^(?:-?\d+|-?\d*[.,]\d+)px$/i.test(str)) {
- return (rootFontSize = parseFloat(str.slice(0, -2)));
+ rootFontSize = parseFloat(str.slice(0, -2));
+ rootFontSizeCache.set(element, rootFontSize);
+ return rootFontSize;
}
const msg = `TreeView warning: computed root element font size '${str}'`;
console.warn(`${msg} is not a pixel value. Using fallback.`);
rootFontSize = -1;
+ rootFontSizeCache.set(element, rootFontSize);
}
if (rootFontSize < 0) {
diff --git a/src/ansys/visor/visor-client/src/utils/portal.js b/src/ansys/visor/visor-client/src/utils/portal.js
new file mode 100644
index 00000000..f8e2b7e1
--- /dev/null
+++ b/src/ansys/visor/visor-client/src/utils/portal.js
@@ -0,0 +1,53 @@
+/**
+ * Helpers to resolve a scoped portal root for UI elements that would
+ * otherwise be appended to `document.body` (tooltips, popups, invisible
+ * DOM containers used by wasm). Prefer an element with the
+ * `.visor-embed-style` class (added by host integrations), then fall back
+ * to the client container id `VisorContainer`, and finally to a dedicated
+ * `visor-portal-root` element created under `document.body`.
+ *
+ * A single JS implementation is kept (no separate .ts version) so there is
+ * one source of truth; JSDoc types below give TS/TSX consumers proper typing.
+ *
+ * @returns {HTMLElement}
+ */
+export function getPortalRoot() {
+ const doc = typeof document !== 'undefined' ? document : null;
+ if (!doc) {
+ // Non-browser environment (SSR). Return a harmless stub to avoid
+ // throwing in places that call this helper during static analysis.
+ // Callers should guard when used outside the browser.
+ return /** @type {HTMLElement} */ ({});
+ }
+
+ const el = doc.querySelector('.visor-embed-style') || doc.getElementById('VisorContainer');
+ if (el) return /** @type {HTMLElement} */ (el);
+
+ let portalRoot = doc.getElementById('visor-portal-root');
+ if (!portalRoot) {
+ portalRoot = doc.createElement('div');
+ portalRoot.id = 'visor-portal-root';
+ portalRoot.setAttribute('data-visor-portal', '1');
+ doc.body.appendChild(portalRoot);
+ }
+
+ return portalRoot;
+}
+
+/**
+ * @param {HTMLElement} el
+ * @returns {void}
+ */
+export function appendToPortal(el) {
+ getPortalRoot().appendChild(el);
+}
+
+/**
+ * @param {HTMLElement} el
+ * @returns {void}
+ */
+export function removeFromPortal(el) {
+ if (el && el.parentElement) {
+ el.parentElement.removeChild(el);
+ }
+}