Skip to content

Commit a3a4e18

Browse files
committed
fix(ui): 修复 Tailwind var() 令牌色透明度修饰符静默失效致弹窗全透明
Tailwind v3 对 var() 形式的令牌色应用 /透明度 修饰符时静默丢弃, 明亮主题下 ContextMenu 背景变为全透明。改用 color-mix 方案。
1 parent ed9e5d7 commit a3a4e18

2 files changed

Lines changed: 91 additions & 60 deletions

File tree

‎client/src/components/ui/ContextMenu.tsx‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
/**
22
* @ai-context: UI 基础组件(shadcn/radix 封装):ContextMenu。
3+
* 定位机制:Radix ContextMenu 的菜单锚点取自触发器真实收到的 contextmenu 事件坐标,
4+
* 因此挂载后向隐藏触发器派发携带 position 坐标的原生事件来打开菜单;
5+
* 绝不可强制传 open(锚点未初始化会导致菜单固定在屏幕左上角)。
36
*/
4-
import React, { useCallback } from 'react';
7+
import React, { useCallback, useEffect, useRef } from 'react';
58
import * as RadixContextMenu from '@radix-ui/react-context-menu';
69
import { motion } from 'framer-motion';
710
import { cn } from '@/lib/utils';
@@ -120,6 +123,8 @@ export const ContextMenu = <C,>({
120123
onSelect,
121124
onClose,
122125
}: ContextMenuProps<C>) => {
126+
const triggerRef = useRef<HTMLSpanElement>(null);
127+
123128
const handleSelect = useCallback(
124129
(itemKey: string) => {
125130
onSelect(itemKey, context);
@@ -128,10 +133,24 @@ export const ContextMenu = <C,>({
128133
[onSelect, context, onClose],
129134
);
130135

136+
// 向隐藏触发器派发带坐标的 contextmenu 事件,让 Radix 记录锚点并打开菜单;
137+
// position 变化时(菜单未关闭又右键另一目标)重新派发以重定位
138+
useEffect(() => {
139+
triggerRef.current?.dispatchEvent(new MouseEvent('contextmenu', {
140+
bubbles: true,
141+
cancelable: true,
142+
clientX: position.x,
143+
clientY: position.y,
144+
}));
145+
}, [position.x, position.y]);
146+
131147
return (
132-
<RadixContextMenu.Root open onOpenChange={(v) => { if (!v) onClose(); }}>
148+
<RadixContextMenu.Root onOpenChange={(v) => { if (!v) onClose(); }}>
133149
{/* Hidden trigger at target position */}
134150
<RadixContextMenu.Trigger
151+
ref={triggerRef}
152+
// 阻止合成事件冒泡到页面的 onContextMenu,避免重复触发 handleContextMenu
153+
onContextMenu={(e) => e.stopPropagation()}
135154
style={{
136155
position: 'fixed',
137156
left: position.x,
@@ -147,7 +166,7 @@ export const ContextMenu = <C,>({
147166

148167
<RadixContextMenu.Portal>
149168
<RadixContextMenu.Content
150-
avoidCollisions={false}
169+
collisionPadding={8}
151170
onCloseAutoFocus={(e: Event) => e.preventDefault()}
152171
className={cn(
153172
'z-[9999] bg-bg-elevated/90 backdrop-blur-2xl border border-border/50 rounded-kb-md shadow-xl py-1 min-w-[160px]',

‎client/tailwind.config.js‎

Lines changed: 69 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,110 @@
11
/** @type {import('tailwindcss').Config} */
2+
3+
/**
4+
* 令牌色 + 透明度修饰符支持(如 bg-bg-elevated/90、border-border/40)。
5+
* Tailwind v3 无法对纯 var() 颜色应用 /alpha 修饰符(此前这些类静默丢失,
6+
* 导致明亮主题下弹窗/面板背景全透明),用 color-mix 按透明度混入 transparent
7+
* 实现(Chromium 111+ / Electron 35 支持)。
8+
*/
9+
const tokenColor = (variable) => ({ opacityValue }) =>
10+
opacityValue === undefined
11+
? `var(${variable})`
12+
: `color-mix(in srgb, var(${variable}) calc(${opacityValue} * 100%), transparent)`;
13+
214
module.exports = {
315
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
416
darkMode: ['selector', '[data-theme="dark"]'],
517
theme: {
618
extend: {
719
colors: {
820
brand: {
9-
50: 'var(--kb-brand-50)',
10-
100: 'var(--kb-brand-100)',
11-
200: 'var(--kb-brand-200)',
12-
300: 'var(--kb-brand-300)',
13-
400: 'var(--kb-brand-400)',
14-
500: 'var(--kb-brand-500)',
15-
600: 'var(--kb-brand-600)',
16-
700: 'var(--kb-brand-700)',
17-
800: 'var(--kb-brand-800)',
18-
900: 'var(--kb-brand-900)',
21+
50: tokenColor('--kb-brand-50'),
22+
100: tokenColor('--kb-brand-100'),
23+
200: tokenColor('--kb-brand-200'),
24+
300: tokenColor('--kb-brand-300'),
25+
400: tokenColor('--kb-brand-400'),
26+
500: tokenColor('--kb-brand-500'),
27+
600: tokenColor('--kb-brand-600'),
28+
700: tokenColor('--kb-brand-700'),
29+
800: tokenColor('--kb-brand-800'),
30+
900: tokenColor('--kb-brand-900'),
1931
},
2032
accent: {
21-
DEFAULT: 'var(--accent)',
22-
foreground: 'var(--accent-foreground)',
23-
50: 'var(--kb-accent-50)',
24-
100: 'var(--kb-accent-100)',
25-
200: 'var(--kb-accent-200)',
26-
300: 'var(--kb-accent-300)',
27-
400: 'var(--kb-accent-400)',
28-
500: 'var(--kb-accent-500)',
29-
600: 'var(--kb-accent-600)',
30-
700: 'var(--kb-accent-700)',
31-
800: 'var(--kb-accent-800)',
32-
900: 'var(--kb-accent-900)',
33+
DEFAULT: tokenColor('--accent'),
34+
foreground: tokenColor('--accent-foreground'),
35+
50: tokenColor('--kb-accent-50'),
36+
100: tokenColor('--kb-accent-100'),
37+
200: tokenColor('--kb-accent-200'),
38+
300: tokenColor('--kb-accent-300'),
39+
400: tokenColor('--kb-accent-400'),
40+
500: tokenColor('--kb-accent-500'),
41+
600: tokenColor('--kb-accent-600'),
42+
700: tokenColor('--kb-accent-700'),
43+
800: tokenColor('--kb-accent-800'),
44+
900: tokenColor('--kb-accent-900'),
3345
},
3446
pomodoro: { DEFAULT: '#5B8A72', light: '#AAC9B5' },
3547
note: { DEFAULT: '#6B9BD2', light: '#ADD6FF' },
3648
flashcard: { DEFAULT: '#7BC4B8', light: '#B8E0D8' },
3749
feynman: { DEFAULT: '#C4956A', light: '#DEBB92' },
3850
classroom: { DEFAULT: '#14B8A6', light: '#5EEAD4' },
3951
/* 深海静谧功能色 */
40-
focus: { DEFAULT: 'var(--kb-focus-blue)' },
41-
amber: { DEFAULT: 'var(--kb-amber)' },
42-
moss: { DEFAULT: 'var(--kb-moss-green)' },
43-
cyber: { DEFAULT: 'var(--kb-cyber-cyan)' },
44-
'stone-purple': 'var(--kb-stone-purple)',
52+
focus: { DEFAULT: tokenColor('--kb-focus-blue') },
53+
amber: { DEFAULT: tokenColor('--kb-amber') },
54+
moss: { DEFAULT: tokenColor('--kb-moss-green') },
55+
cyber: { DEFAULT: tokenColor('--kb-cyber-cyan') },
56+
'stone-purple': tokenColor('--kb-stone-purple'),
4557
bg: {
46-
primary: 'var(--kb-bg-primary)',
47-
secondary: 'var(--kb-bg-secondary)',
48-
tertiary: 'var(--kb-bg-tertiary)',
49-
elevated: 'var(--kb-bg-elevated)',
58+
primary: tokenColor('--kb-bg-primary'),
59+
secondary: tokenColor('--kb-bg-secondary'),
60+
tertiary: tokenColor('--kb-bg-tertiary'),
61+
elevated: tokenColor('--kb-bg-elevated'),
5062
},
5163
text: {
52-
primary: 'var(--kb-text-primary)',
53-
secondary: 'var(--kb-text-secondary)',
54-
tertiary: 'var(--kb-text-tertiary)',
55-
inverse: 'var(--kb-text-inverse)',
64+
primary: tokenColor('--kb-text-primary'),
65+
secondary: tokenColor('--kb-text-secondary'),
66+
tertiary: tokenColor('--kb-text-tertiary'),
67+
inverse: tokenColor('--kb-text-inverse'),
5668
},
5769
border: {
58-
DEFAULT: 'var(--kb-border-default)',
59-
strong: 'var(--kb-border-strong)',
70+
DEFAULT: tokenColor('--kb-border-default'),
71+
strong: tokenColor('--kb-border-strong'),
6072
},
6173
semantic: {
62-
success: 'var(--kb-color-success)',
63-
warning: 'var(--kb-color-warning)',
64-
error: 'var(--kb-color-error)',
65-
info: 'var(--kb-color-info)',
74+
success: tokenColor('--kb-color-success'),
75+
warning: tokenColor('--kb-color-warning'),
76+
error: tokenColor('--kb-color-error'),
77+
info: tokenColor('--kb-color-info'),
6678
},
6779
// shadcn/ui 兼容色
68-
background: 'var(--background)',
69-
foreground: 'var(--foreground)',
80+
background: tokenColor('--background'),
81+
foreground: tokenColor('--foreground'),
7082
card: {
71-
DEFAULT: 'var(--card)',
72-
foreground: 'var(--card-foreground)',
83+
DEFAULT: tokenColor('--card'),
84+
foreground: tokenColor('--card-foreground'),
7385
},
7486
popover: {
75-
DEFAULT: 'var(--popover)',
76-
foreground: 'var(--popover-foreground)',
87+
DEFAULT: tokenColor('--popover'),
88+
foreground: tokenColor('--popover-foreground'),
7789
},
7890
primary: {
79-
DEFAULT: 'var(--primary)',
80-
foreground: 'var(--primary-foreground)',
91+
DEFAULT: tokenColor('--primary'),
92+
foreground: tokenColor('--primary-foreground'),
8193
},
8294
secondary: {
83-
DEFAULT: 'var(--secondary)',
84-
foreground: 'var(--secondary-foreground)',
95+
DEFAULT: tokenColor('--secondary'),
96+
foreground: tokenColor('--secondary-foreground'),
8597
},
8698
muted: {
87-
DEFAULT: 'var(--muted)',
88-
foreground: 'var(--muted-foreground)',
99+
DEFAULT: tokenColor('--muted'),
100+
foreground: tokenColor('--muted-foreground'),
89101
},
90102
destructive: {
91-
DEFAULT: 'var(--destructive)',
92-
foreground: 'var(--destructive-foreground)',
103+
DEFAULT: tokenColor('--destructive'),
104+
foreground: tokenColor('--destructive-foreground'),
93105
},
94-
input: 'var(--input)',
95-
ring: 'var(--ring)',
106+
input: tokenColor('--input'),
107+
ring: tokenColor('--ring'),
96108
},
97109
fontFamily: {
98110
sans: ['var(--kb-font-sans)'],

0 commit comments

Comments
 (0)