fix(view,panel): the composer clears the navbar and the keyboard, panel opens on the master

The inset only knew the desktop status bar, so on a phone the composer sat
under the navbar - which paints over a leaf instead of shortening it - and
the software keyboard hid it outright. Every bottom bar is measured now, and
the keyboard is read off `visualViewport`: it shrinks the visible area
without resizing the layout, so no workspace event fires and the panel had
no way to know. Focus re-measures once iOS has finished animating the keys in.

The panel asks the shell to fall back to the latest master, so opening it on
a phone lands in the day's thread rather than on a prompt.

The preview fixtures anchor at midday instead of the wall clock: spread over
hours, a run just after midnight pushed half of them into yesterday and
emptied the "Today" group the switcher smoke checks.
This commit is contained in:
hh
2026-09-05 00:20:33 +02:00
parent a76ee29255
commit cc46abc34f
4 changed files with 76 additions and 10 deletions
+1
View File
@@ -58,6 +58,7 @@
<PanelShell
{client}
companion={state.companion}
fallbackToMaster
{index}
onOpenFile={(path) => host.openNote?.(`мета/бобер/${path}`)}
showFollow
+52 -9
View File
@@ -7,6 +7,13 @@ import App from "./ui/app.svelte";
export const VIEW_TYPE_PANEL = "beaver-panel";
// Bars that paint over a leaf instead of shortening it. On a phone the navbar
// sits under every view, including the one filling the screen.
const BOTTOM_BARS = [".status-bar", ".mobile-navbar", ".mobile-toolbar"];
// iOS animates the keyboard in; the viewport is only honest once it is done.
const KEYBOARD_SETTLE_MS = 350;
interface SavedState {
follow?: unknown;
selected?: unknown;
@@ -79,6 +86,23 @@ export class PanelView extends ItemView {
this.registerEvent(
this.app.workspace.on("layout-change", () => this.inset())
);
// The keyboard resizes the visual viewport only: no workspace event
// fires, so the composer would stay behind it without these two.
const vv = window.visualViewport;
if (vv) {
const onViewport = () => this.inset();
vv.addEventListener("resize", onViewport);
vv.addEventListener("scroll", onViewport);
this.register(() => {
vv.removeEventListener("resize", onViewport);
vv.removeEventListener("scroll", onViewport);
});
}
// Focusing the composer opens the keyboard a beat before the viewport
// settles; re-measure once it has.
this.registerDomEvent(this.contentEl, "focusin", () => {
window.setTimeout(() => this.inset(), KEYBOARD_SETTLE_MS);
});
this.follow(this.app.workspace.getActiveFile());
this.plugin.views.add(this);
this.mountApp();
@@ -86,18 +110,37 @@ export class PanelView extends ItemView {
return Promise.resolve();
}
// Obsidian's status bar floats over the bottom-right of the workspace; a
// leaf that reaches down there gets padding so the composer stays clear.
// Whatever floats over the bottom of this leaf, measured rather than
// guessed: the desktop status bar, the phone's navbar (which the workspace
// does not subtract from a leaf's height), and the software keyboard, which
// shrinks the visual viewport without resizing the layout at all.
private inset(): void {
const bar = document.querySelector<HTMLElement>(".status-bar");
const box = this.contentEl.getBoundingClientRect();
let overlap = 0;
if (bar && box.height > 0) {
const b = bar.getBoundingClientRect();
const crosses = b.left < box.right && b.right > box.left;
overlap = crosses ? Math.max(0, box.bottom - b.top) : 0;
if (box.height <= 0) {
return;
}
this.contentEl.style.setProperty("--beaver-inset-bottom", `${overlap}px`);
let overlap = 0;
for (const bar of BOTTOM_BARS) {
const node = document.querySelector<HTMLElement>(bar);
if (!node || node.offsetParent === null) {
continue;
}
const b = node.getBoundingClientRect();
if (b.height > 0 && b.left < box.right && b.right > box.left) {
overlap = Math.max(overlap, box.bottom - b.top);
}
}
const vv = window.visualViewport;
if (vv) {
// The keyboard eats the bottom of the window; anything of this leaf
// below that line is unreachable until it goes away.
const floor = vv.offsetTop + vv.height;
overlap = Math.max(overlap, box.bottom - floor);
}
this.contentEl.style.setProperty(
"--beaver-inset-bottom",
`${Math.max(0, Math.round(overlap))}px`
);
}
async onClose(): Promise<void> {