From c5187d38a5e509820d92c998f27021dd1a27abad Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Thu, 13 Aug 2026 19:09:16 +0800 Subject: [PATCH] fix(web): align the sidebar Cordis panel with menu and footer conventions The Cordis popup now uses the standard menu surface (layer-3 fill, inverted border, lv3 shadow) so it stays visible in dark mode, closes on outside pointerdown, and anchors to its trigger through a measured fixed position that escapes the sidebar overflow clip. The Cordis and Settings footer triggers share the same 42px row, icon size, padding, and hover fill, and the panel's header, heading, and card spacing is rebalanced. Refs #2526 --- .../src/client/SettingsRoot.module.css | 12 +++--- .../src/client/CordisPanel.module.css | 34 ++++++++--------- .../ui-cordis/src/client/CordisPanel.tsx | 38 ++++++++++++++++--- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/client/ui-settings-general/src/client/SettingsRoot.module.css b/packages/client/ui-settings-general/src/client/SettingsRoot.module.css index 060e8d115b..03e0f6372c 100644 --- a/packages/client/ui-settings-general/src/client/SettingsRoot.module.css +++ b/packages/client/ui-settings-general/src/client/SettingsRoot.module.css @@ -1,19 +1,19 @@ /* Settings shell (figma 501:29904 mask context / 501:29947 panel): sidebar foot trigger row + centered 1080x700 modal panel. The trigger uses the - sidebar's 34px compact row / 36px rail circle rhythm; the + sidebar foot's 42px row / 36px rail circle rhythm; the panel is a two-column layout — 188px nav rail + content column with a 54px header and the 24px-padded options area. */ -/* Trigger row: match the other wide sidebar controls' compact vertical rhythm. */ +/* Trigger row: the 42px foot-row box shared with the Cordis footer action. */ .trigger { flex: none; display: flex; align-items: center; gap: 8px; - width: calc(100% + 8px); - height: 34px; - margin: 4px -4px 4px; - padding: 6px 2px 6px 10px; + width: calc(100% + 4px); + height: 42px; + margin: 4px -2px; + padding: 0 10px 0 8px; box-sizing: border-box; border: none; border-radius: 12px; diff --git a/packages/extensions/ui-cordis/src/client/CordisPanel.module.css b/packages/extensions/ui-cordis/src/client/CordisPanel.module.css index bf72bfecec..1a10f07ee3 100644 --- a/packages/extensions/ui-cordis/src/client/CordisPanel.module.css +++ b/packages/extensions/ui-cordis/src/client/CordisPanel.module.css @@ -6,7 +6,7 @@ display: flex; align-items: center; width: 100%; - height: 49px; + height: 42px; margin: 8px 0 0; } @@ -20,9 +20,10 @@ display: inline-flex; align-items: center; gap: 8px; - width: 100%; - height: 49px; - padding: 0 8px 0 6px; + width: calc(100% + 4px); + height: 42px; + margin: 0 -2px; + padding: 0 10px 0 8px; border: none; border-radius: 12px; background: transparent; @@ -34,7 +35,7 @@ } .badge:hover { - background: var(--dsw-alias-interactive-bg-hover-solid); + background: var(--dsw-alias-interactive-bg-hover); } .badge[data-active] { @@ -77,10 +78,10 @@ gap: 2px; } +/* Fixed so the sidebar's overflow clip cannot cut the 420px surface; the + left/bottom offsets are measured from the trigger before paint. */ .panel { position: fixed; - left: 12px; - bottom: 128px; z-index: 30; display: flex; flex-direction: column; @@ -88,10 +89,10 @@ max-width: calc(100vw - 24px); max-height: 60vh; overflow: hidden; - border: 1px solid var(--dsw-alias-border-l1); + border: 1px solid var(--dsw-alias-border-inverted); border-radius: 12px; - background: var(--dsw-alias-bg-base); - box-shadow: var(--dsw-shadow-lv2); + background: var(--dsw-specific-menu); + box-shadow: var(--dsw-shadow-lv3); /* Only `.body` scrolls; the header remains fixed above it. */ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); @@ -105,19 +106,17 @@ min-height: 44px; padding: 10px 12px; box-sizing: border-box; - border-bottom: 1px solid var(--dsw-alias-border-l2); - background: var(--dsw-alias-bg-base); } .body { flex: 1; min-height: 0; overflow-y: auto; - padding: 4px 12px 12px; + padding: 0 12px 12px; } .title { - font-size: 13px; + font-size: 14px; font-weight: 500; line-height: 20px; color: var(--dsw-alias-label-primary); @@ -137,7 +136,7 @@ /* Session headings replace per-row ownership markers. */ .group { - margin: 8px 0 4px; + margin: 8px 0 8px; font-size: 11px; font-weight: 500; line-height: 16px; @@ -159,10 +158,9 @@ display: flex; flex-direction: column; gap: 8px; - padding: 10px 12px; + padding: 14px 12px 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; - background: var(--dsw-alias-bg-base); } .row[data-cordis-awaiting] { @@ -252,7 +250,7 @@ padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; - background: var(--dsw-alias-bg-base); + background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; } diff --git a/packages/extensions/ui-cordis/src/client/CordisPanel.tsx b/packages/extensions/ui-cordis/src/client/CordisPanel.tsx index 946e9d9dfa..42f1586dae 100644 --- a/packages/extensions/ui-cordis/src/client/CordisPanel.tsx +++ b/packages/extensions/ui-cordis/src/client/CordisPanel.tsx @@ -1,6 +1,6 @@ /** Frame-wide dynamic Plugin inventory, approvals, versions, and lifecycle actions. */ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' import type { ButtonHTMLAttributes, ReactNode } from 'react' import { IconCheckOutline16, IconCloseOutline16, IconCordisPluginOutline14, IconPlayOutline16, @@ -119,6 +119,34 @@ export function CordisPanel({ const [pending, setPending] = useState>(new Set()) const [actionErrors, setActionErrors] = useState>(new Map()) const visibleRequests = useRef>(new Set()) + const rootRef = useRef(null) + const [anchor, setAnchor] = useState<{ left: number; bottom: number }>() + + // The panel is position: fixed (the sidebar clips overflow), so it hugs the + // trigger through a measured offset instead of document flow. + useLayoutEffect(() => { + if (!open) return + const place = (): void => { + const rect = rootRef.current?.getBoundingClientRect() + if (rect !== undefined) { + setAnchor({ left: rect.left, bottom: window.innerHeight - rect.top + 8 }) + } + } + place() + window.addEventListener('resize', place) + return () => { window.removeEventListener('resize', place) } + }, [open]) + + useEffect(() => { + if (!open) return + const closeOutside = (event: PointerEvent): void => { + if (event.target instanceof Node && !rootRef.current?.contains(event.target)) { + setOpen(false) + } + } + document.addEventListener('pointerdown', closeOutside) + return () => { document.removeEventListener('pointerdown', closeOutside) } + }, [open]) useEffect(() => { const now = new Set() @@ -420,9 +448,9 @@ export function CordisPanel({ } return ( -
- {open && ( -
+
+ {open && anchor !== undefined && ( +
{t('panel.title')}
@@ -458,7 +486,7 @@ export function CordisPanel({ aria-expanded={open} onClick={() => { setOpen(value => !value) }} > - + {wide && ( <> {t('panel.trigger')}