mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Adapt composer clearance to Obsidian status bar
This commit is contained in:
parent
e0b3c29c2e
commit
f7c91d880a
4 changed files with 135 additions and 2 deletions
|
|
@ -15,6 +15,7 @@ interface ChatPanelShellMount {
|
|||
props: ChatPanelShellProps;
|
||||
stateStore: ChatStateStore;
|
||||
unsubscribe: () => void;
|
||||
stopStatusBarClearanceSync: () => void;
|
||||
}
|
||||
|
||||
const shellMounts = new WeakMap<HTMLElement, ChatPanelShellMount>();
|
||||
|
|
@ -62,8 +63,10 @@ export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShe
|
|||
const existing = shellMounts.get(container);
|
||||
if (existing?.stateStore === props.stateStore) {
|
||||
existing.props = props;
|
||||
syncStatusBarClearance(container);
|
||||
} else {
|
||||
existing?.unsubscribe();
|
||||
existing?.stopStatusBarClearanceSync();
|
||||
shellMounts.set(container, {
|
||||
props,
|
||||
stateStore: props.stateStore,
|
||||
|
|
@ -72,6 +75,7 @@ export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShe
|
|||
if (!mount) return;
|
||||
renderMountedSlots(container, mount.props);
|
||||
}),
|
||||
stopStatusBarClearanceSync: startStatusBarClearanceSync(container),
|
||||
});
|
||||
}
|
||||
renderMountedSlots(container, props);
|
||||
|
|
@ -79,7 +83,9 @@ export function renderChatPanelShell(container: HTMLElement, props: ChatPanelShe
|
|||
|
||||
export function unmountChatPanelShell(container: HTMLElement | null): void {
|
||||
if (!container) return;
|
||||
shellMounts.get(container)?.unsubscribe();
|
||||
const mount = shellMounts.get(container);
|
||||
mount?.unsubscribe();
|
||||
mount?.stopStatusBarClearanceSync();
|
||||
shellMounts.delete(container);
|
||||
unmountSlotRoots(container);
|
||||
container.replaceChildren();
|
||||
|
|
@ -142,3 +148,80 @@ function renderKey(renderVersion: number, snapshot: ChatPanelSlotSnapshot): stri
|
|||
function ensureBody(container: HTMLElement): HTMLElement {
|
||||
return container.querySelector<HTMLElement>(":scope > .codex-panel__body") ?? container.createDiv({ cls: "codex-panel__body" });
|
||||
}
|
||||
|
||||
function startStatusBarClearanceSync(container: HTMLElement): () => void {
|
||||
const win = container.ownerDocument.defaultView;
|
||||
if (!win) return noop;
|
||||
|
||||
const cleanupCallbacks: (() => void)[] = [];
|
||||
let observedStatusBar: HTMLElement | null = null;
|
||||
let statusBarMutationObserver: MutationObserver | null = null;
|
||||
let statusBarResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const observeStatusBar = (): void => {
|
||||
const statusBar = container.ownerDocument.querySelector<HTMLElement>(".status-bar");
|
||||
if (statusBar === observedStatusBar) return;
|
||||
statusBarMutationObserver?.disconnect();
|
||||
statusBarResizeObserver?.disconnect();
|
||||
statusBarMutationObserver = null;
|
||||
statusBarResizeObserver = null;
|
||||
observedStatusBar = statusBar;
|
||||
if (!statusBar) return;
|
||||
|
||||
statusBarMutationObserver = new win.MutationObserver(() => {
|
||||
syncStatusBarClearance(container);
|
||||
});
|
||||
statusBarMutationObserver.observe(statusBar, { attributes: true, attributeFilter: ["class", "style"] });
|
||||
|
||||
const ResizeObserverCtor = (win as Window & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver;
|
||||
if (ResizeObserverCtor) {
|
||||
statusBarResizeObserver = new ResizeObserverCtor(() => {
|
||||
syncStatusBarClearance(container);
|
||||
});
|
||||
statusBarResizeObserver.observe(statusBar);
|
||||
}
|
||||
};
|
||||
|
||||
const sync = (): void => {
|
||||
observeStatusBar();
|
||||
syncStatusBarClearance(container);
|
||||
};
|
||||
|
||||
const bodyObserver = new win.MutationObserver(sync);
|
||||
bodyObserver.observe(container.ownerDocument.body, { attributes: true, attributeFilter: ["class", "style"], childList: true });
|
||||
cleanupCallbacks.push(() => {
|
||||
bodyObserver.disconnect();
|
||||
});
|
||||
|
||||
win.addEventListener("resize", sync);
|
||||
cleanupCallbacks.push(() => {
|
||||
win.removeEventListener("resize", sync);
|
||||
});
|
||||
sync();
|
||||
|
||||
return () => {
|
||||
for (const cleanup of cleanupCallbacks) cleanup();
|
||||
statusBarMutationObserver?.disconnect();
|
||||
statusBarResizeObserver?.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function syncStatusBarClearance(container: HTMLElement): void {
|
||||
container.style.setProperty("--codex-panel-status-bar-clearance", `${String(statusBarClearance(container))}px`);
|
||||
}
|
||||
|
||||
function statusBarClearance(container: HTMLElement): number {
|
||||
const win = container.ownerDocument.defaultView;
|
||||
const statusBar = container.ownerDocument.querySelector<HTMLElement>(".status-bar");
|
||||
if (!win || !statusBar) return 0;
|
||||
const style = win.getComputedStyle(statusBar);
|
||||
if (style.display === "none" || style.visibility === "hidden" || style.position !== "fixed") return 0;
|
||||
const rectHeight = statusBar.getBoundingClientRect().height;
|
||||
if (Number.isFinite(rectHeight) && rectHeight > 0) return Math.ceil(rectHeight);
|
||||
const computedHeight = Number.parseFloat(style.height);
|
||||
return Number.isFinite(computedHeight) && computedHeight > 0 ? Math.ceil(computedHeight) : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
--codex-panel-icon-button-block-size: calc(var(--codex-panel-control-icon-size) + var(--codex-panel-control-gap) * 2);
|
||||
--codex-panel-composer-control-size: var(--codex-panel-icon-button-block-size);
|
||||
--codex-panel-composer-min-height: calc((var(--codex-panel-composer-control-size) * 2) + var(--codex-panel-control-gap));
|
||||
--codex-panel-status-bar-clearance: var(--status-bar-height, calc(var(--codex-panel-section-gap) * 3));
|
||||
--codex-panel-status-bar-clearance: 0px;
|
||||
--codex-panel-bottom-safe-area: max(env(safe-area-inset-bottom, 0px), var(--codex-panel-status-bar-clearance));
|
||||
--codex-panel-toolbar-text-color: var(--text-muted);
|
||||
--codex-panel-toolbar-text-size: var(--font-ui-small);
|
||||
|
|
|
|||
|
|
@ -114,6 +114,46 @@ describe("ChatPanelShell", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("sets composer bottom clearance only for fixed visible Obsidian status bars", async () => {
|
||||
const store = createChatStateStore();
|
||||
const container = document.createElement("div");
|
||||
const statusBar = document.createElement("div");
|
||||
statusBar.className = "status-bar";
|
||||
document.body.appendChild(statusBar);
|
||||
document.body.appendChild(container);
|
||||
Object.defineProperty(statusBar, "getBoundingClientRect", {
|
||||
configurable: true,
|
||||
value: () => ({ height: 26, width: 100, top: 0, right: 100, bottom: 26, left: 0, x: 0, y: 0, toJSON: () => ({}) }),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
statusBar.style.display = "flex";
|
||||
statusBar.style.position = "fixed";
|
||||
renderChatPanelShell(container, shellRenderers(store));
|
||||
await settleShellEffects();
|
||||
});
|
||||
expect(container.style.getPropertyValue("--codex-panel-status-bar-clearance")).toBe("26px");
|
||||
|
||||
await act(async () => {
|
||||
statusBar.style.position = "static";
|
||||
renderChatPanelShell(container, shellRenderers(store));
|
||||
await settleShellEffects();
|
||||
});
|
||||
expect(container.style.getPropertyValue("--codex-panel-status-bar-clearance")).toBe("0px");
|
||||
|
||||
await act(async () => {
|
||||
statusBar.style.display = "none";
|
||||
renderChatPanelShell(container, shellRenderers(store));
|
||||
await settleShellEffects();
|
||||
});
|
||||
expect(container.style.getPropertyValue("--codex-panel-status-bar-clearance")).toBe("0px");
|
||||
|
||||
await act(async () => {
|
||||
unmountChatPanelShell(container);
|
||||
});
|
||||
statusBar.remove();
|
||||
});
|
||||
|
||||
it("unmounts existing slot roots before rebuilding a damaged shell scaffold", async () => {
|
||||
const store = createChatStateStore();
|
||||
const container = document.createElement("div");
|
||||
|
|
|
|||
|
|
@ -133,6 +133,16 @@ describe("chat toolbar CSS", () => {
|
|||
expect(composerMetaFatal).toContain("padding-inline-start: calc(var(--size-4-1) / 2)");
|
||||
});
|
||||
|
||||
it("lets the shell provide Obsidian status bar composer clearance", () => {
|
||||
const tokenScope = /^(?<selectors>(?:\.[^{]+,\n)*\.[^{]+) \{(?<body>[^}]+)\}/m.exec(styles)?.groups?.["body"] ?? "";
|
||||
const clearanceLine = tokenScope
|
||||
.split("\n")
|
||||
.find((line) => line.trim().startsWith("--codex-panel-status-bar-clearance:"))
|
||||
?.trim();
|
||||
|
||||
expect(clearanceLine).toBe("--codex-panel-status-bar-clearance: 0px;");
|
||||
});
|
||||
|
||||
it("keeps composer status accessible summary visually hidden", () => {
|
||||
const summary = /\.codex-panel__composer-meta-summary \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const visual = /\.codex-panel__composer-meta-status-visual \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
|
|
|||
Loading…
Reference in a new issue