Merge pull request #181 from Cooper-X-Oak/codex/windows-native-titlebar-off
fix(windows): 收紧主窗口 shell 外观契约
This commit is contained in:
7 files changed
+265
-43
No files matched your search
@@ -15,18 +15,39 @@ function assertMatch(source, pattern, name) {
|
||||
const raw = await readFile(new URL('../src-tauri/tauri.conf.json', import.meta.url), 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
const capsuleWindow = config.app.windows.find((window) => window.label === 'capsule');
|
||||
const mainWindow = config.app.windows.find((window) => window.label === 'main');
|
||||
const libRs = await readFile(new URL('../src-tauri/src/lib.rs', import.meta.url), 'utf-8');
|
||||
const coordinatorRs = await readFile(new URL('../src-tauri/src/coordinator.rs', import.meta.url), 'utf-8');
|
||||
const capsuleTsx = await readFile(new URL('../src/components/Capsule.tsx', import.meta.url), 'utf-8');
|
||||
const capsuleLayoutTs = await readFile(new URL('../src/lib/capsuleLayout.ts', import.meta.url), 'utf-8');
|
||||
const windowChromeTsx = await readFile(new URL('../src/components/WindowChrome.tsx', import.meta.url), 'utf-8');
|
||||
const floatingShellTsx = await readFile(new URL('../src/components/FloatingShell.tsx', import.meta.url), 'utf-8');
|
||||
|
||||
if (!capsuleWindow) {
|
||||
throw new Error('capsule window config missing');
|
||||
}
|
||||
if (!mainWindow) {
|
||||
throw new Error('main window config missing');
|
||||
}
|
||||
assertEqual(capsuleWindow.width, 220, 'windows capsule config keeps translation-capable width baseline');
|
||||
assertEqual(capsuleWindow.height, 110, 'windows capsule config keeps translation-capable height baseline');
|
||||
assertEqual(capsuleWindow.transparent, true, 'capsule window should keep transparent visuals');
|
||||
assertEqual(capsuleWindow.alwaysOnTop, true, 'capsule window should stay above the focused app while recording');
|
||||
assertEqual(mainWindow.decorations, false, 'windows main window should use only custom titlebar');
|
||||
assertEqual(mainWindow.visible, false, 'windows main window should stay hidden until the intended first show point');
|
||||
|
||||
if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) {
|
||||
throw new Error('windows frameless shell should expose explicit resize handles');
|
||||
}
|
||||
|
||||
if (!/startResizeDragging\(direction\)/.test(windowChromeTsx)) {
|
||||
throw new Error('windows resize handles should delegate edge dragging to Tauri');
|
||||
}
|
||||
|
||||
if (!/borderRadius:\s*'var\(--ol-window-console-radius\)'/.test(floatingShellTsx)) {
|
||||
throw new Error('floating shell should consume the shared window-console radius');
|
||||
}
|
||||
|
||||
assertMatch(
|
||||
coordinatorRs,
|
||||
/let visible = !matches!\(state,\s*CapsuleState::Idle\);/,
|
||||
|
||||
@@ -53,8 +53,11 @@ objc2-foundation = "0.2"
|
||||
objc2-app-kit = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
raw-window-handle = "0.6"
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
|
||||
@@ -105,12 +105,18 @@ pub fn run() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use window_vibrancy::apply_mica;
|
||||
if let Err(e) = apply_mica(&main, None) {
|
||||
log::warn!("[main] mica failed: {e}");
|
||||
}
|
||||
// The window starts hidden so Windows native chrome can be disabled before
|
||||
// the first show; doing this after the native frame is visible is unreliable.
|
||||
if let Err(e) = main.set_decorations(false) {
|
||||
log::warn!("[main] disable native decorations failed: {e}");
|
||||
}
|
||||
if let Err(e) = apply_mica(&main, None) {
|
||||
log::warn!("[main] mica failed: {e}");
|
||||
}
|
||||
apply_windows_rounded_frame(&main);
|
||||
}
|
||||
if let Err(e) = main.show() {
|
||||
log::warn!("[main] initial show failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,10 +220,16 @@ pub fn run() {
|
||||
RunEvent::Reopen { .. } => show_main_window(app),
|
||||
RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
if let tauri::WindowEvent::CloseRequested { ref api, .. } = event {
|
||||
api.prevent_close();
|
||||
hide_main_window(app);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if matches!(event, tauri::WindowEvent::Resized(_) | tauri::WindowEvent::ScaleFactorChanged { .. }) {
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
apply_windows_rounded_frame(&main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RunEvent::Exit => {
|
||||
@@ -229,6 +241,97 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn apply_windows_rounded_frame<R: Runtime>(window: &tauri::WebviewWindow<R>) {
|
||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use windows::Win32::Foundation::{BOOL, HWND, RECT};
|
||||
use windows::Win32::Graphics::Dwm::{
|
||||
DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_ROUND,
|
||||
};
|
||||
use windows::Win32::Graphics::Gdi::{CreateRoundRectRgn, SetWindowRgn, HRGN};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongW, GetWindowRect, SetWindowLongW, SetWindowPos, GWL_STYLE, SWP_FRAMECHANGED,
|
||||
SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WS_CAPTION, WS_THICKFRAME,
|
||||
};
|
||||
|
||||
let handle = match window.window_handle().map(|h| h.as_raw()) {
|
||||
Ok(RawWindowHandle::Win32(handle)) => handle,
|
||||
Ok(other) => {
|
||||
log::warn!("[main] unexpected raw window handle for DWM frame: {other:?}");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[main] read raw window handle failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);
|
||||
|
||||
unsafe {
|
||||
let style = GetWindowLongW(hwnd, GWL_STYLE);
|
||||
let desired_style = (style | WS_THICKFRAME.0 as i32) & !(WS_CAPTION.0 as i32);
|
||||
if style != desired_style {
|
||||
SetWindowLongW(hwnd, GWL_STYLE, desired_style);
|
||||
if let Err(e) = SetWindowPos(
|
||||
hwnd,
|
||||
HWND::default(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED,
|
||||
) {
|
||||
log::warn!("[main] refresh native frame after style update failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if window.is_maximized().unwrap_or(false) {
|
||||
let _ = SetWindowRgn(hwnd, HRGN::default(), BOOL(1));
|
||||
return;
|
||||
}
|
||||
|
||||
let corner_preference = DWMWCP_ROUND;
|
||||
if let Err(e) = DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||
&corner_preference as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of_val(&corner_preference) as u32,
|
||||
) {
|
||||
log::warn!("[main] set DWM rounded corners failed: {e}");
|
||||
}
|
||||
|
||||
// Remove DWM's fallback 1px light border; the React shell draws the visual stroke.
|
||||
let border_color_none: u32 = 0xFFFFFFFE;
|
||||
if let Err(e) = DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_BORDER_COLOR,
|
||||
&border_color_none as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of_val(&border_color_none) as u32,
|
||||
) {
|
||||
log::warn!("[main] remove DWM border color failed: {e}");
|
||||
}
|
||||
|
||||
let mut rect = RECT::default();
|
||||
if let Err(e) = GetWindowRect(hwnd, &mut rect) {
|
||||
log::warn!("[main] read window rect for rounded region failed: {e}");
|
||||
return;
|
||||
}
|
||||
let width = rect.right - rect.left;
|
||||
let height = rect.bottom - rect.top;
|
||||
if width <= 0 || height <= 0 {
|
||||
return;
|
||||
}
|
||||
let region = CreateRoundRectRgn(0, 0, width + 1, height + 1, 18, 18);
|
||||
if region.is_invalid() {
|
||||
log::warn!("[main] create rounded window region failed");
|
||||
return;
|
||||
}
|
||||
if SetWindowRgn(hwnd, region, BOOL(1)) == 0 {
|
||||
log::warn!("[main] apply rounded window region failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn restart_app(app: AppHandle) {
|
||||
// macOS:自动更新会让新装的 .app 带 com.apple.quarantine(无论 Tauri updater
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"minWidth": 980,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"decorations": true,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"shadow": true,
|
||||
"hiddenTitle": true,
|
||||
|
||||
@@ -255,7 +255,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
flex: 1, minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: os === 'mac' ? 20 : 14,
|
||||
borderRadius: 'var(--ol-window-console-radius)',
|
||||
border: '0.5px solid rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 1px 0 rgba(255,255,255,0.8) inset, 0 8px 24px -12px rgba(15,17,22,0.10), 0 2px 6px -2px rgba(15,17,22,0.06)',
|
||||
display: 'flex',
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
// WindowChrome.tsx — frosted outer frame + raised inner console pattern.
|
||||
// The OUTER frame is a translucent shell with a tinted backdrop showing through.
|
||||
// The INNER content lives in a single raised card that floats above it.
|
||||
//
|
||||
// Layout per window:
|
||||
// ┌─ frosted outer ───────────────────────────────┐
|
||||
// │ [titlebar] │
|
||||
// │ ┌─ raised console (white, shadow) ─┐ │
|
||||
// │ │ sidebar │ main │ │
|
||||
// │ └──────────────────────────────────┘ │
|
||||
// │ [icon footer] │
|
||||
// └───────────────────────────────────────────────┘
|
||||
|
||||
import { type CSSProperties, type ReactNode } from 'react';
|
||||
import { useState, type CSSProperties, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
|
||||
export type OS = 'mac' | 'win' | 'linux';
|
||||
|
||||
@@ -30,6 +18,21 @@ export function detectOS(): OS {
|
||||
|
||||
const MAC_TITLEBAR_HEIGHT = 36;
|
||||
const MAC_SYSTEM_CONTROLS_RESERVED_WIDTH = 80;
|
||||
export const WIN_TITLEBAR_HEIGHT = 36;
|
||||
export const WIN_WINDOW_RADIUS = 10;
|
||||
export const WIN_CONSOLE_RADIUS = 10;
|
||||
const WIN_RESIZE_EDGE = 6;
|
||||
const WIN_RESIZE_CORNER = 14;
|
||||
|
||||
type ResizeDirection =
|
||||
| 'East'
|
||||
| 'North'
|
||||
| 'NorthEast'
|
||||
| 'NorthWest'
|
||||
| 'South'
|
||||
| 'SouthEast'
|
||||
| 'SouthWest'
|
||||
| 'West';
|
||||
|
||||
interface WindowChromeProps {
|
||||
os?: OS;
|
||||
@@ -38,19 +41,32 @@ interface WindowChromeProps {
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
export function WindowChrome({ os = 'mac', title = 'OpenLess', children, height = 800 }: WindowChromeProps) {
|
||||
export function WindowChrome({
|
||||
os = 'mac',
|
||||
title = 'OpenLess',
|
||||
children,
|
||||
height = 800,
|
||||
}: WindowChromeProps) {
|
||||
const shellRadius = os === 'mac' ? 20 : os === 'win' ? WIN_WINDOW_RADIUS : 14;
|
||||
const consoleRadius = os === 'mac' ? 20 : os === 'win' ? WIN_CONSOLE_RADIUS : 14;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
'--ol-window-shell-radius': `${shellRadius}px`,
|
||||
'--ol-window-console-radius': `${consoleRadius}px`,
|
||||
'--ol-window-titlebar-height': `${os === 'mac' ? MAC_TITLEBAR_HEIGHT : WIN_TITLEBAR_HEIGHT}px`,
|
||||
width: '100%',
|
||||
height,
|
||||
position: 'relative',
|
||||
borderRadius: os === 'mac' ? 20 : 14,
|
||||
boxShadow: 'var(--ol-shadow-xl)',
|
||||
borderRadius: 'var(--ol-window-shell-radius)',
|
||||
boxShadow: os === 'win'
|
||||
? '0 22px 54px -28px rgba(15, 17, 22, 0.38), 0 10px 30px -20px rgba(15, 17, 22, 0.22), 0 0 0 0.5px rgba(0, 0, 0, 0.10)'
|
||||
: 'var(--ol-shadow-xl)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '0.5px solid rgba(0,0,0,.10)',
|
||||
border: os === 'win' ? '1px solid rgba(255,255,255,0.18)' : '0.5px solid rgba(0,0,0,.10)',
|
||||
background: `
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0) 60%),
|
||||
radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%),
|
||||
@@ -58,11 +74,10 @@ export function WindowChrome({ os = 'mac', title = 'OpenLess', children, height
|
||||
`,
|
||||
backdropFilter: 'blur(40px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
|
||||
}}
|
||||
} as CSSProperties}
|
||||
>
|
||||
{os === 'win' && <WinTitleBar title={title} />}
|
||||
{/* macOS:三色窗口按钮交给系统绘制和定位。这里只保留顶部拖动区,
|
||||
并避开系统按钮热区,防止拖动层吞掉 close/minimize/zoom 点击。 */}
|
||||
{os === 'win' && <WindowsResizeHandles />}
|
||||
{os === 'mac' && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
@@ -92,12 +107,14 @@ function WinTitleBar({ title }: WinTitleBarProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 36,
|
||||
height: WIN_TITLEBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
position: 'relative',
|
||||
zIndex: 5,
|
||||
zIndex: 70,
|
||||
borderBottom: '0.5px solid rgba(0, 0, 0, 0.08)',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.20) 0%, rgba(255,255,255,0.08) 100%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -107,24 +124,105 @@ function WinTitleBar({ title }: WinTitleBarProps) {
|
||||
<img src="AppIcon.png" alt="" style={{ width: 14, height: 14, borderRadius: 3 }} />
|
||||
<span style={{ fontSize: 12, color: 'var(--ol-ink-3)', fontWeight: 500 }}>{title}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<button style={winBtnStyle} title={t('windowChrome.minimize')} onClick={() => runWindowsWindowAction('minimize')}>
|
||||
<div style={{ display: 'flex', pointerEvents: 'auto' }}>
|
||||
<WinTitleButton title={t('windowChrome.minimize')} action="minimize">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M0 5h10" stroke="currentColor" strokeWidth="1" /></svg>
|
||||
</button>
|
||||
<button style={winBtnStyle} title={t('windowChrome.maximize')} onClick={() => runWindowsWindowAction('toggleMaximize')}>
|
||||
</WinTitleButton>
|
||||
<WinTitleButton title={t('windowChrome.maximize')} action="toggleMaximize">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" strokeWidth="1" fill="none" /></svg>
|
||||
</button>
|
||||
<button style={winCloseBtnStyle} title={t('windowChrome.close')} onClick={() => runWindowsWindowAction('close')}>
|
||||
</WinTitleButton>
|
||||
<WinTitleButton title={t('windowChrome.close')} action="close" tone="danger">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M0 0L10 10M10 0L0 10" stroke="currentColor" strokeWidth="1" /></svg>
|
||||
</button>
|
||||
</WinTitleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsResizeHandles() {
|
||||
const handles: Array<{
|
||||
direction: ResizeDirection;
|
||||
cursor: CSSProperties['cursor'];
|
||||
style: CSSProperties;
|
||||
}> = [
|
||||
{ direction: 'North', cursor: 'ns-resize', style: { top: 0, left: WIN_RESIZE_CORNER, right: WIN_RESIZE_CORNER, height: WIN_RESIZE_EDGE } },
|
||||
{ direction: 'South', cursor: 'ns-resize', style: { bottom: 0, left: WIN_RESIZE_CORNER, right: WIN_RESIZE_CORNER, height: WIN_RESIZE_EDGE } },
|
||||
{ direction: 'West', cursor: 'ew-resize', style: { top: WIN_RESIZE_CORNER, bottom: WIN_RESIZE_CORNER, left: 0, width: WIN_RESIZE_EDGE } },
|
||||
{ direction: 'East', cursor: 'ew-resize', style: { top: WIN_RESIZE_CORNER, bottom: WIN_RESIZE_CORNER, right: 0, width: WIN_RESIZE_EDGE } },
|
||||
{ direction: 'NorthWest', cursor: 'nwse-resize', style: { top: 0, left: 0, width: WIN_RESIZE_CORNER, height: WIN_RESIZE_CORNER } },
|
||||
{ direction: 'NorthEast', cursor: 'nesw-resize', style: { top: 0, right: 0, width: WIN_RESIZE_CORNER, height: WIN_RESIZE_CORNER } },
|
||||
{ direction: 'SouthWest', cursor: 'nesw-resize', style: { bottom: 0, left: 0, width: WIN_RESIZE_CORNER, height: WIN_RESIZE_CORNER } },
|
||||
{ direction: 'SouthEast', cursor: 'nwse-resize', style: { bottom: 0, right: 0, width: WIN_RESIZE_CORNER, height: WIN_RESIZE_CORNER } },
|
||||
];
|
||||
|
||||
return (
|
||||
<div aria-hidden style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 60 }}>
|
||||
{handles.map(handle => (
|
||||
<div
|
||||
key={handle.direction}
|
||||
onMouseDown={event => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void startResizeDragging(handle.direction);
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
pointerEvents: 'auto',
|
||||
cursor: handle.cursor,
|
||||
...handle.style,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface WinTitleButtonProps {
|
||||
title: string;
|
||||
action: 'minimize' | 'toggleMaximize' | 'close';
|
||||
tone?: 'default' | 'danger';
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function WinTitleButton({ title, action, tone = 'default', children }: WinTitleButtonProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [pressed, setPressed] = useState(false);
|
||||
const danger = tone === 'danger';
|
||||
const background = pressed
|
||||
? danger ? '#c42b1c' : 'rgba(0, 0, 0, 0.12)'
|
||||
: hovered ? danger ? '#e81123' : 'rgba(0, 0, 0, 0.08)'
|
||||
: 'transparent';
|
||||
const color = danger && (hovered || pressed) ? '#fff' : 'var(--ol-ink-3)';
|
||||
|
||||
return (
|
||||
<button
|
||||
style={{ ...winBtnStyle, background, color }}
|
||||
title={title}
|
||||
onClick={() => runWindowsWindowAction(action)}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setHovered(false);
|
||||
setPressed(false);
|
||||
}}
|
||||
onMouseDown={() => setPressed(true)}
|
||||
onMouseUp={() => setPressed(false)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
async function startResizeDragging(direction: ResizeDirection) {
|
||||
try {
|
||||
await getCurrentWindow().startResizeDragging(direction);
|
||||
} catch (error) {
|
||||
console.warn(`[window] Windows resize ${direction} failed`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async function runWindowsWindowAction(action: 'minimize' | 'toggleMaximize' | 'close') {
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const currentWindow = getCurrentWindow();
|
||||
if (action === 'minimize') {
|
||||
await currentWindow.minimize();
|
||||
@@ -150,8 +248,3 @@ const winBtnStyle: CSSProperties = {
|
||||
cursor: 'default',
|
||||
transition: 'background 0.12s ease-out, color 0.12s ease-out',
|
||||
};
|
||||
|
||||
const winCloseBtnStyle: CSSProperties = {
|
||||
...winBtnStyle,
|
||||
color: 'var(--ol-ink-3)',
|
||||
};
|
||||
@@ -3,17 +3,19 @@ html, body, #root {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
Reference in new issue
Block a user