fix(ui): support legacy WebView dialog viewports

This commit is contained in:
zipg 2026-07-22 15:26:23 +08:00 committed by GitHub
parent 4d2b60326d
commit 127328a4ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 74 additions and 17 deletions

View File

@ -381,7 +381,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
</div>
<Dialog v-model:open="scheduleDialogOpen">
<DialogContent class="max-h-[min(760px,calc(100dvh-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
<DialogContent class="max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
<DialogHeader>
<DialogTitle>{{ editingScheduleId ? t("databaseBackup.editSchedule") : t("databaseBackup.addSchedule") }}</DialogTitle>
</DialogHeader>

View File

@ -6377,7 +6377,7 @@ function openExternalUrl(url: string) {
.connection-dialog-content {
display: flex;
flex-direction: column;
max-height: calc(100dvh - 2rem);
max-height: calc(var(--dbx-viewport-height) - 2rem);
}
@media (min-width: 640px) {

View File

@ -106,10 +106,10 @@ const isMaximized = ref(false);
const dialogStyle = computed(() => {
if (isMaximized.value) {
return {
width: "100vw",
height: "100vh",
maxWidth: "100vw",
maxHeight: "100vh",
width: "100%",
height: "100%",
maxWidth: "100%",
maxHeight: "100%",
borderRadius: "0",
};
}
@ -133,6 +133,13 @@ function toggleMaximize() {
isMaximized.value = !isMaximized.value;
}
function handleDialogEscape(event: KeyboardEvent) {
if (!showOptionsPanel.value) return;
event.preventDefault();
showOptionsPanel.value = false;
}
let resizeObserver: ResizeObserver | null = null;
let saveTimeout: number | null = null;
@ -679,7 +686,7 @@ const targetConnectionInfo = computed(() => {
<template>
<Dialog v-model:open="open">
<DialogContent :class="['min-w-[800px] flex flex-col overflow-hidden', isMaximized ? '' : 'resize']" :style="dialogStyle" @interact-outside.prevent>
<DialogContent :class="['flex flex-col overflow-hidden', isMaximized ? 'min-w-0' : 'min-w-[800px] resize']" :portal-class="isMaximized ? 'p-0' : undefined" :style="dialogStyle" @interact-outside.prevent @escape-key-down="handleDialogEscape">
<Button variant="ghost" size="icon-sm" class="absolute top-2 right-10 z-10" @click="toggleMaximize">
<Maximize2 v-if="!isMaximized" class="w-4 h-4" />
<Minimize2 v-else class="w-4 h-4" />

View File

@ -0,0 +1,25 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../SchemaDiffDialog.vue", import.meta.url), "utf8");
describe("SchemaDiffDialog fullscreen layout", () => {
it("fits the dialog to its portal instead of the viewport width", () => {
expect(dialogSource).toContain('width: "100%"');
expect(dialogSource).toContain('height: "100%"');
expect(dialogSource).not.toContain('width: "100vw"');
expect(dialogSource).not.toContain('height: "100vh"');
});
it("removes the normal dialog gutter and minimum width while maximized", () => {
expect(dialogSource).toContain(":portal-class=\"isMaximized ? 'p-0' : undefined\"");
expect(dialogSource).toContain("isMaximized ? 'min-w-0' : 'min-w-[800px] resize'");
});
it("closes the options panel before allowing Escape to dismiss the dialog", () => {
expect(dialogSource).toContain('@escape-key-down="handleDialogEscape"');
expect(dialogSource).toContain("if (!showOptionsPanel.value) return;");
expect(dialogSource).toContain("event.preventDefault();");
expect(dialogSource).toContain("showOptionsPanel.value = false;");
});
});

View File

@ -172,7 +172,9 @@ const settingsRootComponent = computed(() => (isSettingsPage.value ? "div" : Dia
const settingsRootProps = computed(() => (isSettingsPage.value ? {} : { open: props.open === true }));
const settingsRootClass = computed(() => (isSettingsPage.value ? "h-full min-h-0 overflow-hidden bg-background" : ""));
const settingsContentComponent = computed(() => (isSettingsPage.value ? "div" : DialogContent));
const settingsContentClass = computed(() => (isSettingsPage.value ? "flex h-full min-h-0 flex-col gap-4 overflow-hidden bg-background p-4" : "h-[min(660px,calc(100dvh-80px))] !max-w-[min(920px,calc(100vw-32px))] grid-rows-[auto_minmax(0,1fr)] gap-3 p-4 sm:!max-w-[min(920px,calc(100vw-48px))]"));
const settingsContentClass = computed(() =>
isSettingsPage.value ? "flex h-full min-h-0 flex-col gap-4 overflow-hidden bg-background p-4" : "h-[min(660px,calc(var(--dbx-viewport-height)-80px))] !max-w-[min(920px,calc(100vw-32px))] grid-rows-[auto_minmax(0,1fr)] gap-3 p-4 sm:!max-w-[min(920px,calc(100vw-48px))]",
);
const settingsTitleComponent = computed(() => (isSettingsPage.value ? "h2" : DialogTitle));
function onSettingsRootOpenChange(value: boolean) {

View File

@ -822,7 +822,7 @@ watch(targetMode, (mode) => {
<template>
<Dialog v-model:open="open">
<DialogScrollContent class="flex max-h-[calc(100dvh-6rem)] min-h-0 flex-col overflow-hidden sm:max-w-[980px]" :trap-focus="false" @interact-outside.prevent>
<DialogScrollContent class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 flex-col overflow-hidden sm:max-w-[980px]" :trap-focus="false" @interact-outside.prevent>
<DialogHeader class="shrink-0 pr-8">
<DialogTitle class="flex items-center gap-2 text-base">
<FileUp class="h-4 w-4" />

View File

@ -133,7 +133,7 @@ defineExpose({ show });
<template>
<Dialog v-model:open="open">
<DialogContent class="h-[min(760px,calc(100dvh-2rem))] flex flex-col overflow-hidden sm:max-w-3xl">
<DialogContent class="h-[min(760px,calc(var(--dbx-viewport-height)-2rem))] flex flex-col overflow-hidden sm:max-w-3xl">
<DialogHeader>
<DialogTitle>{{ t("extension.manageTitle") }}</DialogTitle>
</DialogHeader>

View File

@ -182,7 +182,7 @@ function closeDialog() {
<template>
<Dialog :open="props.open" @update:open="(value) => emit('update:open', value)">
<DialogContent class="h-[min(760px,calc(100dvh-2rem))] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-[900px]">
<DialogContent class="h-[min(760px,calc(var(--dbx-viewport-height)-2rem))] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-[900px]">
<DialogHeader>
<DialogTitle>{{ title }}</DialogTitle>
</DialogHeader>

View File

@ -268,7 +268,7 @@ function openResult(item: SearchResultItem) {
<template>
<Dialog v-model:open="dialogOpen">
<DialogScrollContent class="flex max-h-[calc(100dvh-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0">
<DialogScrollContent class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0">
<DialogHeader class="shrink-0 border-b px-5 py-4">
<DialogTitle class="flex items-center gap-2">
<Search class="h-5 w-5" />

View File

@ -5,7 +5,7 @@ const dialogSource = readFileSync(new URL("../DatabaseSearchDialog.vue", import.
describe("DatabaseSearchDialog layout", () => {
it("keeps the header and footer visible within the dynamic viewport", () => {
expect(dialogSource).toContain('class="flex max-h-[calc(100dvh-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0"');
expect(dialogSource).toContain('class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 max-w-4xl flex-col overflow-hidden gap-0 p-0"');
expect(dialogSource).toContain('<DialogHeader class="shrink-0 border-b px-5 py-4">');
expect(dialogSource).toContain('<DialogFooter class="shrink-0 border-t px-5 py-3">');
});

View File

@ -438,7 +438,7 @@ watch(
<template>
<Dialog :open="open" @update:open="handleOpenChange">
<DialogScrollContent class="flex max-h-[calc(100dvh-6rem)] min-h-0 min-w-0 flex-col overflow-hidden sm:max-w-[860px]" :trap-focus="false" @interact-outside.prevent>
<DialogScrollContent class="flex max-h-[calc(var(--dbx-viewport-height)-6rem)] min-h-0 min-w-0 flex-col overflow-hidden sm:max-w-[860px]" :trap-focus="false" @interact-outside.prevent>
<DialogHeader class="shrink-0">
<DialogTitle class="flex items-center gap-2">
<FileCode class="w-4 h-4" />

View File

@ -42,7 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 relative grid max-h-[calc(100dvh-2rem)] w-full max-w-sm gap-4 overflow-hidden rounded-xl border border-border p-4 text-sm shadow-lg duration-100 outline-none pointer-events-auto',
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 relative grid max-h-[calc(var(--dbx-viewport-height)-2rem)] w-full max-w-sm gap-4 overflow-hidden rounded-xl border border-border p-4 text-sm shadow-lg duration-100 outline-none pointer-events-auto',
props.class,
)
"

View File

@ -26,7 +26,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<div class="fixed inset-0 z-50 grid place-items-center p-4 pointer-events-none">
<DialogContent
data-slot="dialog-content"
:class="cn('relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-4 shadow-lg duration-200 sm:rounded-lg md:w-full pointer-events-auto', props.class)"
:class="cn('relative z-50 grid max-h-[calc(var(--dbx-viewport-height)-6rem)] w-full max-w-lg my-8 gap-4 border border-border bg-background p-4 shadow-lg duration-200 sm:rounded-lg md:w-full pointer-events-auto', props.class)"
v-bind="{ ...$attrs, ...forwarded }"
@pointer-down-outside="
(event) => {

View File

@ -2,6 +2,9 @@ import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const globalsCss = readFileSync(new URL("../globals.css", import.meta.url), "utf8");
const dialogContentSource = readFileSync(new URL("../../components/ui/dialog/DialogContent.vue", import.meta.url), "utf8");
const dialogScrollContentSource = readFileSync(new URL("../../components/ui/dialog/DialogScrollContent.vue", import.meta.url), "utf8");
const connectionDialogSource = readFileSync(new URL("../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("legacy WebView CSS fallbacks", () => {
it("scopes component overrides to WebViews without OKLCH support", () => {
@ -24,4 +27,17 @@ describe("legacy WebView CSS fallbacks", () => {
expect(tabsNestingDepth).toBeGreaterThan(0);
expect(nestingDepth).toBe(0);
});
it("falls back to the legacy viewport height when dynamic viewport units are unavailable", () => {
const fallback = globalsCss.indexOf("--dbx-viewport-height: 100vh;");
const supports = globalsCss.indexOf("@supports (height: 100dvh)");
const enhanced = globalsCss.indexOf("--dbx-viewport-height: min(100vh, 100dvh);");
expect(fallback).toBeGreaterThan(-1);
expect(supports).toBeGreaterThan(fallback);
expect(enhanced).toBeGreaterThan(supports);
expect(dialogContentSource).toContain("max-h-[calc(var(--dbx-viewport-height)-2rem)]");
expect(dialogScrollContentSource).toContain("max-h-[calc(var(--dbx-viewport-height)-6rem)]");
expect(connectionDialogSource).toContain("max-height: calc(var(--dbx-viewport-height) - 2rem);");
});
});

View File

@ -120,6 +120,13 @@
--sidebar-border: rgb(229 229 229);
--sidebar-ring: rgb(161 161 161);
--dbx-window-top-border: rgb(0 0 0 / 0.35);
--dbx-viewport-height: 100vh;
}
@supports (height: 100dvh) {
:root {
--dbx-viewport-height: min(100vh, 100dvh);
}
}
.dark {

View File

@ -17,7 +17,7 @@ test("SQL file execution dialog SFC compiles", () => {
});
test("SQL file execution dialog keeps actions visible within narrow viewports", () => {
assert.match(dialogSource, /DialogScrollContent class="[^"]*max-h-\[calc\(100dvh-6rem\)\][^"]*flex-col[^"]*overflow-hidden/);
assert.match(dialogSource, /DialogScrollContent class="[^"]*max-h-\[calc\(var\(--dbx-viewport-height\)-6rem\)\][^"]*flex-col[^"]*overflow-hidden/);
assert.match(dialogSource, /<DialogHeader class="shrink-0">/);
assert.match(dialogSource, /class="grid min-h-0 min-w-0 flex-1 gap-4 overflow-y-auto py-3"/);
assert.match(dialogSource, /class="grid grid-cols-1 gap-3 sm:grid-cols-2"/);