[eric] dashboard: docked APPS suppress their tether too; tiled geometry never transitions (occluded-window transitions pin at t=0 and freeze tiles); tiles + dock measures re-apply on window reveal

This commit is contained in:
ciregenz
2026-07-28 18:16:01 -07:00
parent 224dc04696
commit 37f7a7a8db
4 changed files with 17 additions and 3 deletions
@@ -271,11 +271,13 @@ const BrowserCard: React.FC<Props> = ({
// A RO only fires on slot RESIZE; the chat tiling/untiling MOVES the slot without resizing the
// window, so re-measure on camera writes + settle timers or the docked card lags behind.
window.addEventListener('openswarm:canvas-pan-changed', measure);
document.addEventListener('visibilitychange', measure);
const timers = [60, 250, 700].map((ms) => window.setTimeout(measure, ms));
return () => {
ro.disconnect();
window.removeEventListener('resize', measure);
window.removeEventListener('openswarm:canvas-pan-changed', measure);
document.removeEventListener('visibilitychange', measure);
timers.forEach((tm) => window.clearTimeout(tm));
};
// dockParentCard x/y/w/h are re-measure triggers: the slot's client rect moves with the chat card.
@@ -197,11 +197,13 @@ const DashboardViewCard: React.FC<Props> = ({
// A RO only fires on slot RESIZE; the chat tiling/untiling MOVES the slot without resizing the
// window, so re-measure on camera writes + settle timers or the docked card lags behind.
window.addEventListener('openswarm:canvas-pan-changed', measure);
document.addEventListener('visibilitychange', measure);
const timers = [60, 250, 700].map((ms) => window.setTimeout(measure, ms));
return () => {
ro.disconnect();
window.removeEventListener('resize', measure);
window.removeEventListener('openswarm:canvas-pan-changed', measure);
document.removeEventListener('visibilitychange', measure);
timers.forEach((tm) => window.clearTimeout(tm));
};
}, [dockedTo, dockParentExpanded, dockParentTiled, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);
@@ -123,7 +123,7 @@ export function useTiledStyle(
? el
: (el.parentElement as HTMLElement | null) ?? el;
const posProps = ['left', 'top'];
const sizeProps = ['width', 'height', 'transform', 'transform-origin'];
const sizeProps = ['width', 'height', 'transform', 'transform-origin', 'transition'];
const clearAll = (): void => {
posProps.forEach((pr) => posEl.style.removeProperty(pr));
sizeProps.forEach((pr) => el.style.removeProperty(pr));
@@ -142,10 +142,18 @@ export function useTiledStyle(
el.style.setProperty('height', `${s.height}px`, 'important');
el.style.setProperty('transform', s.transform, 'important');
el.style.setProperty('transform-origin', s.transformOrigin, 'important');
// Transitions OUTRANK inline important in the cascade, and in an occluded window they pin at
// t=0 forever, freezing the tile at stale geometry. Tiled cards track the camera instantly
// by design, so geometry must never animate here.
el.style.setProperty('transition', 'none', 'important');
};
apply();
window.addEventListener('openswarm:canvas-pan-changed', apply);
window.addEventListener('resize', apply);
// An occluded window freezes rAF (framer, the tracker below), so geometry set while hidden can
// land stale; re-apply the moment the window is visible again.
document.addEventListener('visibilitychange', apply);
window.addEventListener('focus', apply);
// The workspace can change size WITHOUT a window resize (chrome collapsing on fullscreen-enter,
// a banner appearing). These inline writes OVERRIDE the class styles, so they must re-measure on
// the same signals the React path uses or the tile keeps the pre-collapse viewport (the
@@ -170,6 +178,8 @@ export function useTiledStyle(
return () => {
window.removeEventListener('openswarm:canvas-pan-changed', apply);
window.removeEventListener('resize', apply);
document.removeEventListener('visibilitychange', apply);
window.removeEventListener('focus', apply);
ro.disconnect();
timers.forEach((tm) => window.clearTimeout(tm));
window.cancelAnimationFrame(raf);
@@ -266,8 +266,8 @@ export function useTethers({
// An "app:<output_id>" glow key targets a VIEW card (AppAgent driving an app); everything else is a browser card.
const glowTarget = (id: string) => (id.startsWith('app:') ? viewCards[id.slice(4)] : browserCards[id]);
for (const [browserId, { sourceId, fading, label }] of Object.entries(glowingBrowserCards)) {
// A docked browser renders INSIDE its chat; an arrow from the chat to it points at nothing.
if (browserCards[browserId]?.docked_to) continue;
// A docked card (browser OR app) renders INSIDE its chat; an arrow to it points at nothing.
if (glowTarget(browserId)?.docked_to) continue;
if (sourceIsCollapsedChat(sourceId)) continue;
const t = cardTether(
glowTarget(browserId),