дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(workbench): 优化侧栏悬停、选中和提示框样式 (#2094)
Sidebar buttons used a larger radius than the rest of the IDE, and their tooltips opened below the button, often over neighbouring stripe buttons. Stripe buttons now use a 6px radius, and their tooltips open toward the inside of the window, with a dark background in light themes. Siblings covered: every theme and plugin theme falls back to its base tooltip tokens, long tooltips flip correctly at the window edge, the shortcut hint stays readable, and the More button gets the same tooltip. Co-authored-by: PLZY <[email protected]>
Цей коміт міститься в:
1 джерело
f04164d7ee
коміт
bb86e626a2
9 files changed
+233
-12
No files matched your search
@@ -103,6 +103,10 @@ Three things still layer on top of it:
|
||||
The selection is remembered. When the plugin is disabled or uninstalled the
|
||||
built-in theme returns.
|
||||
|
||||
Stripe tooltips use `--bg-stripe-tooltip`, `--fg-stripe-tooltip`,
|
||||
`--border-stripe-tooltip` and `--shadow-stripe-tooltip`. A plugin that omits
|
||||
these tokens inherits its corresponding base tooltip tokens, including in light themes.
|
||||
|
||||
## Building, packaging, installing
|
||||
|
||||
There is no build step. The package *is* the folder.
|
||||
@@ -324,7 +328,7 @@ a token.
|
||||
|
||||
<!-- BEGIN GENERATED TOKEN REFERENCE: pnpm gen:theme-docs -->
|
||||
|
||||
243 themeable tokens: 121 in the UI theme file, 122 in the editor colour scheme file.
|
||||
247 themeable tokens: 125 in the UI theme file, 122 in the editor colour scheme file.
|
||||
|
||||
### Surfaces (UI theme)
|
||||
|
||||
@@ -571,6 +575,10 @@ a token.
|
||||
|
||||
| Token | Islands Dark | Islands Light |
|
||||
|---|---|---|
|
||||
| `--bg-stripe-tooltip` | `var(--bg-tooltip)` | `#27282e` |
|
||||
| `--fg-stripe-tooltip` | `var(--fg-tooltip)` | `#ffffff` |
|
||||
| `--border-stripe-tooltip` | `var(--border-tooltip)` | `#383a42` |
|
||||
| `--shadow-stripe-tooltip` | `var(--shadow-tooltip)` | `0 2px 8px rgba(0, 0, 0, 0.24)` |
|
||||
| `--bg-tooltip` | `#33353b` | `#ffffff` |
|
||||
| `--fg-tooltip` | `#d1d3d9` | `#000000` |
|
||||
| `--border-tooltip` | `#33353b` | `#d1d3d9` |
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
|
||||
test("stripe tooltips stay inside the viewport with a gap on either side and at corners", async ({
|
||||
workbench: page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 520, height: 760 });
|
||||
await page.evaluate(() => {
|
||||
const button = document.createElement("button");
|
||||
button.id = "tooltip-anchor";
|
||||
button.dataset.tooltipVerbatim = "";
|
||||
button.dataset.tooltipShortcut = "⌘1";
|
||||
button.style.cssText = "position:fixed;width:40px;height:40px;z-index:2000";
|
||||
document.body.append(button);
|
||||
});
|
||||
const button = page.locator("#tooltip-anchor");
|
||||
const tooltip = page.getByRole("tooltip");
|
||||
for (const side of ["left", "right"]) {
|
||||
for (const top of [0, 360, 720]) {
|
||||
for (const long of [false, true]) {
|
||||
await page.mouse.move(260, 200);
|
||||
await button.evaluate(
|
||||
(button, { side, top, long }) => {
|
||||
button.dataset.tooltipStripe = side;
|
||||
button.dataset.tooltip = long ? "A long tool window title ".repeat(12) : "Project";
|
||||
button.style.left = side === "left" ? "0" : "480px";
|
||||
button.style.top = `${top}px`;
|
||||
},
|
||||
{ side, top, long },
|
||||
);
|
||||
await button.hover();
|
||||
await expect(tooltip).toHaveAttribute("data-placed", "true");
|
||||
const a = (await button.boundingBox())!;
|
||||
const t = (await tooltip.boundingBox())!;
|
||||
expect(t.x).toBeGreaterThanOrEqual(6);
|
||||
expect(t.y).toBeGreaterThanOrEqual(6);
|
||||
expect(t.x + t.width).toBeLessThanOrEqual(514);
|
||||
expect(t.y + t.height).toBeLessThanOrEqual(754);
|
||||
expect(
|
||||
t.x >= a.x + a.width + 5 ||
|
||||
t.x + t.width <= a.x - 5 ||
|
||||
t.y >= a.y + a.height + 5 ||
|
||||
t.y + t.height <= a.y - 5,
|
||||
).toBe(true);
|
||||
if (!long) expect(side === "left" ? t.x > a.x : t.x < a.x).toBe(true);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(tooltip).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("stripe themes inherit base tooltip tokens and keep shortcut text readable", async ({ workbench: page }) => {
|
||||
const button = page.locator('[data-tooltip-stripe="left"][data-tooltip-shortcut]').first();
|
||||
for (const theme of ["dark", "light", "dark-hc", "solarized-light", "solarized-dark", "one-light", "one-dark"]) {
|
||||
await page.mouse.move(700, 400);
|
||||
await page.evaluate((theme) => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = theme.endsWith("light") ? "light" : theme === "dark-hc" ? "dark-hc" : "dark";
|
||||
if (theme.includes("-light") || ["solarized-dark", "one-dark"].includes(theme)) root.dataset.colorTheme = theme;
|
||||
else delete root.dataset.colorTheme;
|
||||
}, theme);
|
||||
await button.hover();
|
||||
const tooltip = page.getByRole("tooltip");
|
||||
await expect(tooltip).toHaveAttribute("data-placed", "true");
|
||||
const ratio = await tooltip.evaluate((tooltip) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = canvas.height = 1;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const background = getComputedStyle(tooltip).backgroundColor;
|
||||
const foreground = getComputedStyle(tooltip.children[1]!).color;
|
||||
const luminance = () => {
|
||||
const rgb = [...ctx.getImageData(0, 0, 1, 1).data].slice(0, 3).map((v) => {
|
||||
const s = v / 255;
|
||||
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return rgb[0]! * 0.2126 + rgb[1]! * 0.7152 + rgb[2]! * 0.0722;
|
||||
};
|
||||
ctx.fillStyle = background;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const bg = luminance();
|
||||
ctx.fillStyle = foreground;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const fg = luminance();
|
||||
return (Math.max(bg, fg) + 0.05) / (Math.min(bg, fg) + 0.05);
|
||||
});
|
||||
if (["dark", "light", "dark-hc"].includes(theme)) expect(ratio, theme).toBeGreaterThanOrEqual(4.5);
|
||||
if (theme !== "light") {
|
||||
const aliases = await page.evaluate(() => {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
return ["bg", "fg", "border", "shadow"].every(
|
||||
(role) => style.getPropertyValue(`--${role}-stripe-tooltip`) === style.getPropertyValue(`--${role}-tooltip`),
|
||||
);
|
||||
});
|
||||
expect(aliases, theme).toBe(true);
|
||||
}
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
for (const kind of ["light", "dark"]) {
|
||||
await page.mouse.move(700, 400);
|
||||
await page.evaluate(async (kind) => {
|
||||
const root = document.documentElement;
|
||||
delete root.dataset.colorTheme;
|
||||
root.dataset.theme = kind;
|
||||
root.dataset.pluginTheme = "test/base-tooltip";
|
||||
const path = "/src/features/plugins/bridge.ts";
|
||||
const { pluginThemeCss } = await import(/* @vite-ignore */ path);
|
||||
const style = document.createElement("style");
|
||||
style.id = "test-plugin-theme";
|
||||
style.textContent = pluginThemeCss({
|
||||
kind,
|
||||
tokens: {
|
||||
"--bg-tooltip": "#183452",
|
||||
"--fg-tooltip": "#ffffff",
|
||||
"--border-tooltip": "#abcdef",
|
||||
"--shadow-tooltip": "0 2px 8px #00000040",
|
||||
},
|
||||
});
|
||||
document.body.append(style);
|
||||
}, kind);
|
||||
await button.hover();
|
||||
const tooltip = page.getByRole("tooltip");
|
||||
await expect(tooltip).toHaveCSS("background-color", "rgb(24, 52, 82)");
|
||||
await expect(tooltip).toHaveCSS("color", "rgb(255, 255, 255)");
|
||||
await expect(tooltip).toHaveCSS("border-top-color", "rgb(171, 205, 239)");
|
||||
await page.evaluate(() => document.querySelector("#test-plugin-theme")!.remove());
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
});
|
||||
@@ -83,6 +83,10 @@
|
||||
"--traffic-off-border": "#363e49",
|
||||
|
||||
// Additive
|
||||
"--bg-stripe-tooltip": "var(--bg-tooltip)",
|
||||
"--fg-stripe-tooltip": "var(--fg-tooltip)",
|
||||
"--border-stripe-tooltip": "var(--border-tooltip)",
|
||||
"--shadow-stripe-tooltip": "var(--shadow-tooltip)",
|
||||
"--bg-tooltip": "#2a333f",
|
||||
"--fg-tooltip": "#c9d1dc",
|
||||
"--border-tooltip": "#2a333f",
|
||||
|
||||
@@ -300,6 +300,10 @@ html {
|
||||
--dur: 140ms;
|
||||
|
||||
/* additive: overlays, layers, controls */
|
||||
--bg-stripe-tooltip: var(--bg-tooltip);
|
||||
--fg-stripe-tooltip: var(--fg-tooltip);
|
||||
--border-stripe-tooltip: var(--border-tooltip);
|
||||
--shadow-stripe-tooltip: var(--shadow-tooltip);
|
||||
--bg-tooltip: #33353b;
|
||||
--fg-tooltip: #d1d3d9;
|
||||
--border-tooltip: #33353b;
|
||||
@@ -337,7 +341,6 @@ html {
|
||||
--fg-stripe-label: #9fa2a8;
|
||||
--fs-stripe-label: 11px;
|
||||
--stripe-button-w: 58px;
|
||||
--stripe-button-radius: 6px;
|
||||
--bg-tab-selected: #233558;
|
||||
--border-tab-selected: #2e4d89;
|
||||
--bg-tab-selected-inactive: #26282c;
|
||||
@@ -650,6 +653,10 @@ html {
|
||||
--traffic-off-border: #c8c8c8;
|
||||
|
||||
/* additive */
|
||||
--bg-stripe-tooltip: #27282e;
|
||||
--fg-stripe-tooltip: #ffffff;
|
||||
--border-stripe-tooltip: #383a42;
|
||||
--shadow-stripe-tooltip: 0 2px 8px rgba(0, 0, 0, 0.24);
|
||||
--bg-tooltip: #ffffff;
|
||||
--fg-tooltip: #000000;
|
||||
--border-tooltip: #d1d3d9;
|
||||
@@ -909,6 +916,10 @@ html {
|
||||
--terminal-ansi-bright-cyan: var(--ansi-bright-cyan);
|
||||
--terminal-ansi-bright-white: var(--ansi-bright-white);
|
||||
|
||||
--bg-stripe-tooltip: var(--bg-tooltip);
|
||||
--fg-stripe-tooltip: var(--fg-tooltip);
|
||||
--border-stripe-tooltip: var(--border-tooltip);
|
||||
--shadow-stripe-tooltip: var(--shadow-tooltip);
|
||||
--bg-tooltip: #24262b;
|
||||
--fg-tooltip: #ffffff;
|
||||
--border-tooltip: #6f737a;
|
||||
@@ -985,6 +996,10 @@ html {
|
||||
* Keep before appearance options so accessibility preferences still win.
|
||||
*/
|
||||
:root[data-color-theme] {
|
||||
--bg-stripe-tooltip: var(--bg-tooltip);
|
||||
--fg-stripe-tooltip: var(--fg-tooltip);
|
||||
--border-stripe-tooltip: var(--border-tooltip);
|
||||
--shadow-stripe-tooltip: var(--shadow-tooltip);
|
||||
--bg-app: var(--palette-bg);
|
||||
--bg-island: var(--palette-bg);
|
||||
--bg-input: var(--palette-bg);
|
||||
@@ -1230,6 +1245,14 @@ html {
|
||||
--terminal-ansi-bright-white: var(--ansi-bright-white);
|
||||
}
|
||||
|
||||
/* Plugin themes can opt into stripe colours; otherwise their base tooltip tokens win. */
|
||||
:root[data-plugin-theme] {
|
||||
--bg-stripe-tooltip: var(--bg-tooltip);
|
||||
--fg-stripe-tooltip: var(--fg-tooltip);
|
||||
--border-stripe-tooltip: var(--border-tooltip);
|
||||
--shadow-stripe-tooltip: var(--shadow-tooltip);
|
||||
}
|
||||
|
||||
:root[data-color-theme="solarized-light"] {
|
||||
--palette-yellow: #b58900;
|
||||
--palette-orange: #cb4b16;
|
||||
|
||||
@@ -34,6 +34,18 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stripe {
|
||||
color: var(--fg-stripe-tooltip, var(--fg-tooltip));
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-stripe-tooltip, var(--bg-tooltip));
|
||||
border-color: var(--border-stripe-tooltip, var(--border-tooltip));
|
||||
box-shadow: var(--shadow-stripe-tooltip, var(--shadow-tooltip));
|
||||
line-height: 18px;
|
||||
}
|
||||
.stripe .shortcut {
|
||||
color: color-mix(in srgb, var(--fg-stripe-tooltip, var(--fg-tooltip)) 80%, transparent);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tooltip[data-placed] {
|
||||
transition: none;
|
||||
|
||||
@@ -64,4 +64,31 @@ describe("TooltipHost", () => {
|
||||
fireEvent.pointerOut(button);
|
||||
expect(screen.queryByRole("tooltip")).toBeNull();
|
||||
});
|
||||
|
||||
it.each(["left", "right", undefined, "invalid"])("keeps dismissal behavior for %s tooltips", (stripe) => {
|
||||
render(
|
||||
<>
|
||||
<TooltipHost />
|
||||
<button type="button" data-tooltip-stripe={stripe} {...tooltipAttrs("Project", "⌘1", true)}>
|
||||
Project
|
||||
</button>
|
||||
</>,
|
||||
);
|
||||
const button = screen.getByRole("button");
|
||||
for (const dismiss of [
|
||||
() => fireEvent.pointerOut(button),
|
||||
() => fireEvent.pointerDown(button),
|
||||
() => fireEvent.keyDown(button, { key: "Escape" }),
|
||||
() => fireEvent.scroll(document),
|
||||
() => fireEvent.wheel(document),
|
||||
() => fireEvent.blur(window),
|
||||
]) {
|
||||
fireEvent.pointerOver(button);
|
||||
act(() => vi.advanceTimersByTime(TOOLTIP_DELAY));
|
||||
const tooltip = screen.getByRole("tooltip");
|
||||
expect(tooltip.classList.contains(styles.stripe!)).toBe(stripe === "left" || stripe === "right");
|
||||
dismiss();
|
||||
expect(screen.queryByRole("tooltip")).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
* 600ms of hover. Hidden on scroll, key press, pointer down or leave.
|
||||
*/
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { createPortal } from "react-dom";
|
||||
import { computePosition, flip, offset, shift } from "@floating-ui/react-dom";
|
||||
import styles from "./Tooltip.module.css";
|
||||
@@ -27,6 +28,7 @@ interface Shown {
|
||||
x: number;
|
||||
y: number;
|
||||
placed: boolean;
|
||||
stripe?: "left" | "right";
|
||||
}
|
||||
|
||||
export function TooltipHost() {
|
||||
@@ -52,7 +54,15 @@ export function TooltipHost() {
|
||||
if (!text || !el.isConnected) return;
|
||||
visible = true;
|
||||
anchorRef.current = el;
|
||||
setShown({ text, shortcut: el.dataset.tooltipShortcut, x: -9999, y: -9999, placed: false });
|
||||
const stripe = el.dataset.tooltipStripe;
|
||||
setShown({
|
||||
text,
|
||||
shortcut: el.dataset.tooltipShortcut,
|
||||
x: -9999,
|
||||
y: -9999,
|
||||
placed: false,
|
||||
stripe: stripe === "left" || stripe === "right" ? stripe : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const onOver = (e: PointerEvent) => {
|
||||
@@ -103,9 +113,13 @@ export function TooltipHost() {
|
||||
if (!anchor || !tip) return;
|
||||
let cancelled = false;
|
||||
void computePosition(anchor, tip, {
|
||||
placement: "bottom",
|
||||
placement: shown.stripe === "right" ? "left" : shown.stripe === "left" ? "right" : "bottom",
|
||||
strategy: "fixed",
|
||||
middleware: [offset(6), flip({ padding: 6 }), shift({ padding: 6 })],
|
||||
middleware: [
|
||||
offset(6),
|
||||
flip({ padding: 6, ...(shown.stripe && { fallbackAxisSideDirection: "end" }) }),
|
||||
shift({ padding: 6 }),
|
||||
],
|
||||
}).then(({ x, y }) => {
|
||||
if (!cancelled) setShown((s) => (s ? { ...s, x, y, placed: true } : s));
|
||||
});
|
||||
@@ -119,7 +133,7 @@ export function TooltipHost() {
|
||||
<div
|
||||
ref={tipRef}
|
||||
role="tooltip"
|
||||
className={styles.tooltip}
|
||||
className={clsx(styles.tooltip, shown.stripe && styles.stripe)}
|
||||
data-placed={shown.placed || undefined}
|
||||
style={{ left: shown.x, top: shown.y }}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.frame {
|
||||
--stripe-button-radius: var(--radius-popup);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
@@ -246,7 +245,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
border-radius: var(--stripe-button-radius);
|
||||
border-radius: var(--radius-toolbar-button);
|
||||
color: var(--fg-muted);
|
||||
transition:
|
||||
background var(--dur-fast) var(--ease),
|
||||
@@ -267,8 +266,6 @@
|
||||
.stripeButton:hover,
|
||||
.stripeButton[aria-expanded="true"] {
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.stripeButton[aria-expanded="true"] {
|
||||
background: var(--bg-stripe-hover);
|
||||
}
|
||||
.stripeActive,
|
||||
@@ -278,8 +275,8 @@
|
||||
}
|
||||
.stripeFocused,
|
||||
.stripeFocused:hover {
|
||||
background: var(--bg-button-primary);
|
||||
color: var(--fg-button-primary);
|
||||
background: var(--bg-stripe-selected);
|
||||
color: var(--fg-emphasis);
|
||||
--icon-color: currentColor;
|
||||
}
|
||||
.stripeButton:focus-visible {
|
||||
@@ -466,3 +463,9 @@
|
||||
:global(:root[data-tool-window-bg="different"]) .anchor {
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.stripeButton {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -420,6 +420,7 @@ function StripeButton({
|
||||
e.preventDefault();
|
||||
showContextMenu(e, stripeButtonMenu(tw, showName));
|
||||
}}
|
||||
data-tooltip-stripe={side}
|
||||
{...tooltipAttrs(titleOf(tw), shortcut, true)}
|
||||
>
|
||||
<span className={styles.stripeIcon}>
|
||||
@@ -463,6 +464,7 @@ function StripeMore({ windows }: { windows: ToolWindowContribution[] }) {
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
data-tooltip-stripe="left"
|
||||
{...tooltipAttrs("More Tool Windows")}
|
||||
>
|
||||
<Icon name="more-horizontal" size={20} />
|
||||
|
||||
Reference in new issue
Заблокувати користувача