diff --git a/platform_web/components/OperationControls.test.tsx b/platform_web/components/OperationControls.test.tsx
index c8f71a4..0a895f4 100644
--- a/platform_web/components/OperationControls.test.tsx
+++ b/platform_web/components/OperationControls.test.tsx
@@ -22,7 +22,9 @@ describe("shared operation dialogs", () => {
expect(html).toContain('aria-labelledby=');
expect(html).toContain('aria-describedby=');
expect(html).toContain('aria-busy="true"');
+ expect(html).toContain("关闭确认停用");
expect(operationControlsSource).toContain('event.key === "Escape"');
+ expect(operationControlsSource).toContain('document.body.style.overflow = "hidden"');
expect(operationControlsSource).toContain("previousFocus?.focus()");
});
diff --git a/platform_web/components/OperationControls.tsx b/platform_web/components/OperationControls.tsx
index e457a63..0f6d37d 100644
--- a/platform_web/components/OperationControls.tsx
+++ b/platform_web/components/OperationControls.tsx
@@ -32,7 +32,13 @@ export function ConfirmDialog({ open, title, description, confirmLabel, danger,
aria-busy={busy}
onClick={(event) => event.stopPropagation()}
>
-
@@ -75,7 +81,7 @@ export function ManagementDialog({ open, title, description, wide, onClose, chil
aria-describedby={description ? descriptionId : undefined}
onClick={(event) => event.stopPropagation()}
>
-
+
{title}
-
- 背景桌面
-
-
- {themeBackgroundPresets.map((preset) => {
- const isFallbackPreset = preset.id === themeState.backgroundPresetId;
- const isVisiblePreset = isFallbackPreset && !hasCustomBackground;
- return (
-
- );
- })}
+
+ 自定义背景
-
{themeState.backgroundImage ? `当前显示自定义上传背景;${activeBackground.label} 仅作为移除上传后的备用桌面。` : "当前使用内置背景桌面。"}
+
{themeState.backgroundImage ? "当前显示自定义上传背景;移除后回到主题配色背景。" : "当前仅使用主题配色背景。"}
diff --git a/platform_web/theme/base-css.test.js b/platform_web/theme/base-css.test.js
index ed21682..f7148fd 100644
--- a/platform_web/theme/base-css.test.js
+++ b/platform_web/theme/base-css.test.js
@@ -112,4 +112,45 @@ describe("platform web shared theme CSS", () => {
expect(mobileRule).toContain(".app-nav-copy strong");
expect(mobileRule).toContain("display: block");
});
+
+ it("keeps desktop sidebar controls pinned inside the viewport", () => {
+ const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
+ const shellRule = themeCss.slice(themeCss.indexOf(".app-shell {"), themeCss.indexOf(".workspace-background-layer"));
+ const sidebarRule = themeCss.slice(themeCss.indexOf(".app-sidebar {"), themeCss.indexOf(".app-brand {"));
+
+ expect(shellRule).toContain("min-height: 100dvh");
+ expect(sidebarRule).toContain("position: sticky");
+ expect(sidebarRule).toContain("height: 100dvh");
+ expect(sidebarRule).toContain("overflow-y: auto");
+ expect(sidebarRule).toContain("overscroll-behavior: contain");
+ });
+
+ it("keeps dialogs and drawers internally scrollable on short viewports", () => {
+ const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
+ const dialogRule = themeCss.slice(themeCss.indexOf("/* ---- drawer ---- */"), themeCss.indexOf(".plugin-detail-actions"));
+ const confirmStart = themeCss.indexOf("/* ---- confirm dialog ---- */");
+ const confirmRule = themeCss.slice(confirmStart, themeCss.indexOf(".confirm-actions {", confirmStart));
+ const mobileRule = themeCss.slice(themeCss.indexOf("/* ---- narrow screens ---- */"), themeCss.indexOf("/* A state view inside an already framed surface"));
+
+ expect(dialogRule).toContain(".dialog-panel-header");
+ expect(dialogRule).toContain(".drawer-panel > .panel-header");
+ expect(dialogRule).toContain("position: sticky");
+ expect(dialogRule).toContain("overflow-y: auto");
+ expect(dialogRule).toContain("overscroll-behavior: contain");
+ expect(confirmRule).toContain("max-height: calc(100dvh - 32px)");
+ expect(confirmRule).toContain("overflow-y: auto");
+ expect(mobileRule).toContain("max-height: calc(100dvh - 20px)");
+ });
+
+ it("keeps magical card decoration from becoming square background patches", () => {
+ const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
+ const magicalSurfaceRule = themeCss.slice(
+ themeCss.indexOf(':root[data-theme-palette="magical-girl"] .metric-card'),
+ themeCss.indexOf(':root[data-theme-palette="magical-girl"] :where(')
+ );
+
+ expect(magicalSurfaceRule).toContain("background: var(--panel-material)");
+ expect(magicalSurfaceRule).not.toContain("background-size: 56px 56px");
+ expect(magicalSurfaceRule).not.toContain("background-image: var(--corner-sparkle), var(--panel-material)");
+ });
});
diff --git a/platform_web/theme/base.css b/platform_web/theme/base.css
index 0e349f7..ae601db 100644
--- a/platform_web/theme/base.css
+++ b/platform_web/theme/base.css
@@ -102,7 +102,7 @@ button {
/* ---- app shell with themed background ---- */
.app-shell {
- min-height: 100vh;
+ min-height: 100dvh;
display: grid;
grid-template-columns: var(--sidebar-width, 216px) minmax(0, 1fr);
position: relative;
@@ -125,12 +125,12 @@ button {
position: fixed;
inset: 0;
z-index: 0;
- background-image: var(--workspace-background-image), var(--workspace-background-pattern-image);
- background-size: cover, var(--workspace-background-pattern-size);
- background-position: center, var(--workspace-background-pattern-position);
- background-repeat: no-repeat, var(--workspace-background-pattern-repeat);
- filter: var(--workspace-background-pattern-filter);
- opacity: var(--workspace-background-pattern-opacity);
+ background-image: var(--workspace-background-image);
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ filter: none;
+ opacity: 0;
pointer-events: none;
}
@@ -389,13 +389,16 @@ button {
}
.app-sidebar {
- position: relative;
+ position: sticky;
+ top: 0;
z-index: 1;
display: flex;
flex-direction: column;
gap: 14px;
width: var(--sidebar-width, 216px);
min-width: 0;
+ height: 100dvh;
+ max-height: 100dvh;
padding: 16px 10px;
border-right: 1px solid color-mix(in srgb, var(--line) 72%, transparent);
background:
@@ -404,6 +407,10 @@ button {
backdrop-filter: blur(26px) saturate(1.2);
box-shadow: inset -1px 0 0 color-mix(in srgb, var(--rim-light) 24%, transparent), 12px 0 34px rgba(0, 0, 0, 0.34);
transition: width 180ms ease, padding 180ms ease;
+ overflow-x: hidden;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ scrollbar-gutter: stable;
}
.app-brand {
@@ -1028,6 +1035,7 @@ button {
.theme-upload {
display: inline-flex;
align-items: center;
+ justify-content: center;
gap: 6px;
min-height: 32px;
padding: 0 10px;
@@ -1038,9 +1046,16 @@ button {
cursor: pointer;
font-size: 12px;
font-weight: 700;
+ line-height: 1;
+ white-space: nowrap;
box-shadow: inset 0 1px 0 var(--crystal-rim);
}
+.theme-upload svg,
+.theme-upload span {
+ flex: 0 0 auto;
+}
+
.theme-upload:hover,
.theme-upload:focus-visible {
border-color: var(--accent);
@@ -1064,9 +1079,7 @@ button {
background:
radial-gradient(900px 430px at 82% 10%, var(--background-glow-1), transparent 64%),
radial-gradient(740px 420px at 14% 82%, var(--background-glow-3), transparent 62%),
- var(--workspace-background-pattern-image),
var(--background-base);
- background-size: auto, auto, var(--workspace-background-pattern-size), auto;
}
.auth-card {
@@ -1329,15 +1342,9 @@ button {
justify-content: center;
border: 1px solid var(--crystal-rim);
border-radius: 50%;
- background:
- var(--corner-sparkle),
- radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.86), transparent 34%),
- linear-gradient(135deg, var(--pink), var(--accent));
- background-size: 54px 54px, auto, auto;
- background-position: right -5px top -4px, center, center;
- background-repeat: no-repeat;
- color: #ffffff;
- box-shadow: var(--jelly-inset), 0 10px 26px var(--candy-glow), 0 0 18px var(--moonbeam);
+ background: color-mix(in srgb, var(--surface-solid) 36%, transparent);
+ color: var(--ink);
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--rim-light) 28%, transparent);
overflow: hidden;
}
@@ -1481,17 +1488,18 @@ button {
min-height: 30px;
padding: 0 10px;
border-radius: 999px;
- border: 1px solid rgba(255, 255, 255, 0.58);
- background: var(--glass-wash), var(--gold-soft);
- color: #6f4d00;
+ border: 1px solid color-mix(in srgb, var(--line-strong) 54%, rgba(255, 255, 255, 0.16));
+ background: color-mix(in srgb, var(--surface-solid) 78%, rgba(255, 255, 255, 0.08));
+ color: var(--ink);
font-size: 13px;
white-space: nowrap;
- box-shadow: inset 0 1px 0 var(--crystal-rim), 0 8px 20px var(--glass-shadow);
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--rim-light) 26%, transparent);
}
.page-status-ready {
- background: var(--success-soft);
- color: var(--success);
+ background: color-mix(in srgb, var(--success-soft) 72%, var(--surface-solid));
+ border-color: color-mix(in srgb, var(--success) 42%, var(--line));
+ color: color-mix(in srgb, var(--success) 82%, var(--ink));
}
.page-summary-strip {
@@ -1725,10 +1733,7 @@ button {
:root[data-theme-palette="magical-girl"] .confirm-panel,
:root[data-theme-palette="magical-girl"] .plugin-group,
:root[data-theme-palette="magical-girl"] .operation-item {
- background-image: var(--corner-sparkle), var(--panel-material);
- background-size: 56px 56px, auto;
- background-position: right 6px top 4px, center;
- background-repeat: no-repeat, no-repeat;
+ background: var(--panel-material);
overflow: visible;
}
@@ -2521,6 +2526,29 @@ button {
opacity: var(--frame-accessory-opacity);
}
+:where(
+ .metric-card,
+ .overview-card,
+ .console-panel,
+ .catalog-card,
+ .server-card,
+ .server-detail-header,
+ .resource-table-wrap,
+ .provider-table-wrap,
+ .server-table-wrap,
+ .plugin-group,
+ .operation-item,
+ .confirm-panel,
+ .drawer-panel,
+ .server-toolbar,
+ .state-view
+)::after {
+ content: none;
+ background: none;
+ box-shadow: none;
+ filter: none;
+}
+
.resource-table,
.provider-table {
width: 100%;
@@ -3567,12 +3595,14 @@ button {
backdrop-filter: blur(6px);
display: flex;
justify-content: flex-end;
+ overflow-y: auto;
+ overscroll-behavior: contain;
}
.drawer-panel {
width: min(440px, 100%);
height: 100%;
- overflow: auto;
+ max-height: 100dvh;
display: grid;
align-content: start;
gap: 14px;
@@ -3582,7 +3612,9 @@ button {
border-left: 1px solid var(--line);
box-shadow: inset 1px 0 0 var(--crystal-rim), -18px 0 42px var(--glass-shadow);
position: relative;
- overflow: hidden;
+ overflow-x: hidden;
+ overflow-y: auto;
+ overscroll-behavior: contain;
}
.plugin-detail-panel {
@@ -3596,10 +3628,11 @@ button {
}
.management-dialog-panel {
- width: min(720px, 100%);
+ width: min(720px, calc(100vw - 32px));
height: auto;
max-height: min(760px, calc(100dvh - 32px));
- overflow: auto;
+ overflow-x: hidden;
+ overflow-y: auto;
border: 1px solid var(--line);
border-left: 1px solid var(--line);
border-radius: 8px;
@@ -3607,7 +3640,49 @@ button {
}
.management-dialog-wide {
- width: min(920px, 100%);
+ width: min(920px, calc(100vw - 32px));
+}
+
+.management-dialog-backdrop {
+ place-items: center;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+}
+
+:root[data-theme-palette] .drawer-panel,
+:root[data-theme-palette] .management-dialog-panel,
+:root[data-theme-palette] .confirm-panel {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.dialog-panel-header,
+.drawer-panel > .panel-header,
+.confirm-panel > .panel-header {
+ position: sticky;
+ top: -20px;
+ z-index: 4;
+ margin: -20px -20px 12px;
+ padding: 16px 20px 12px;
+ border-bottom: 1px solid color-mix(in srgb, var(--line) 72%, transparent);
+ background: var(--glass-wash), var(--surface-raised);
+ -webkit-backdrop-filter: blur(20px) saturate(1.12);
+ backdrop-filter: blur(20px) saturate(1.12);
+}
+
+.management-dialog-panel .dialog-form > .confirm-actions,
+.drawer-panel > .confirm-actions,
+.confirm-panel > .confirm-actions,
+.runtime-task-actions {
+ position: sticky;
+ bottom: -20px;
+ z-index: 4;
+ margin: 4px -20px -20px;
+ padding: 12px 20px 16px;
+ border-top: 1px solid color-mix(in srgb, var(--line) 72%, transparent);
+ background: var(--glass-wash), var(--surface-raised);
+ -webkit-backdrop-filter: blur(20px) saturate(1.12);
+ backdrop-filter: blur(20px) saturate(1.12);
}
.dialog-description {
@@ -3931,7 +4006,7 @@ button {
padding: 13px;
border: 1px solid var(--line);
border-radius: 8px;
- background: var(--corner-sparkle), var(--jelly-highlight), var(--glass-wash), var(--surface);
+ background: var(--corner-sparkle), var(--surface);
background-size: 52px 52px, auto, auto, auto;
background-position: right 8px top 6px, center, center, center;
background-repeat: no-repeat;
@@ -4225,10 +4300,13 @@ button {
padding: 16px;
background: rgba(61, 36, 65, 0.38);
backdrop-filter: blur(6px);
+ overflow-y: auto;
+ overscroll-behavior: contain;
}
.confirm-panel {
width: min(460px, 100%);
+ max-height: calc(100dvh - 32px);
display: grid;
gap: 14px;
padding: 20px;
@@ -4238,7 +4316,9 @@ button {
backdrop-filter: blur(24px) saturate(1.28);
box-shadow: var(--jelly-inset), inset 0 0 0 1px var(--diamond-line), 0 18px 48px var(--glass-shadow), 0 0 34px var(--moonbeam);
position: relative;
- overflow: hidden;
+ overflow-x: hidden;
+ overflow-y: auto;
+ overscroll-behavior: contain;
}
.confirm-panel h2 {
@@ -4916,6 +4996,38 @@ button {
.drawer-panel {
width: 100%;
+ max-height: 100dvh;
+ }
+
+ .confirm-backdrop {
+ padding: max(10px, env(safe-area-inset-top)) 10px max(10px, env(safe-area-inset-bottom));
+ place-items: stretch center;
+ }
+
+ .confirm-panel,
+ .management-dialog-panel,
+ .management-dialog-wide {
+ width: min(100%, calc(100vw - 20px));
+ max-height: calc(100dvh - 20px);
+ }
+
+ .management-dialog-panel .management-form,
+ .management-dialog-panel .provider-form .form-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .management-dialog-panel .dialog-form > .confirm-actions,
+ .drawer-panel > .confirm-actions,
+ .confirm-panel > .confirm-actions,
+ .runtime-task-actions {
+ justify-content: stretch;
+ }
+
+ .management-dialog-panel .dialog-form > .confirm-actions > *,
+ .drawer-panel > .confirm-actions > *,
+ .confirm-panel > .confirm-actions > *,
+ .runtime-task-actions > * {
+ flex: 1 1 140px;
}
}