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()} > -

{title}

+
+

{title}

+ +

{description}

{children}
@@ -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; } }