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
This commit is contained in:
Yif
2026-08-13 19:09:16 +08:00
parent c1ae458090
commit c5187d38a5
3 changed files with 55 additions and 29 deletions

View File

@@ -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;

View File

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

View File

@@ -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<ReadonlySet<CordisDynamicPluginId>>(new Set())
const [actionErrors, setActionErrors] = useState<ReadonlyMap<CordisDynamicPluginId, string>>(new Map())
const visibleRequests = useRef<Set<ApprovalRequestId>>(new Set())
const rootRef = useRef<HTMLDivElement>(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<ApprovalRequestId>()
@@ -420,9 +448,9 @@ export function CordisPanel({
}
return (
<div className={wide ? css.layer : `${css.layer} ${css.rail}`}>
{open && (
<section className={css.panel} data-cordis-panel aria-label={t('panel.title')}>
<div ref={rootRef} className={wide ? css.layer : `${css.layer} ${css.rail}`}>
{open && anchor !== undefined && (
<section className={css.panel} style={anchor} data-cordis-panel aria-label={t('panel.title')}>
<header className={css.header}>
<span className={css.title}>{t('panel.title')}</span>
</header>
@@ -458,7 +486,7 @@ export function CordisPanel({
aria-expanded={open}
onClick={() => { setOpen(value => !value) }}
>
<IconCordisPluginOutline14 />
<IconCordisPluginOutline14 size={wide ? 16 : 18} />
{wide && (
<>
<span className={css.badgeLabel}>{t('panel.trigger')}</span>