feat: deliver the redesigned desktop workspace

Rebuild downloads, link collection, settings, history, and statistics around a responsive desktop shell with compact account and queue tables, contextual navigation, persistent update affordances, unified overlays, and accessible keyboard interactions.

Add safe history-folder reveal IPC, responsive 2560/1920/1366/1120 coverage, deterministic visual fixtures, focused component regressions, and release-tree exclusions for internal working files. Bump the public application version to 2.0.13.
This commit is contained in:
Sucukdeluxe
2026-08-10 14:15:57 +02:00
parent d844b33501
commit 069babfd54
77 changed files with 17442 additions and 3333 deletions
+38
View File
@@ -0,0 +1,38 @@
import type { ReactElement, ReactNode } from "react";
import { Icon } from "../ui/Icon";
import { buildMainNavigation, type MainView } from "./shell-model";
export interface AppHeaderProps {
activeView: MainView;
onViewChange: (view: MainView) => void;
actions: ReactNode;
}
export function AppHeader({ activeView, onViewChange, actions }: AppHeaderProps): ReactElement {
const navigation = buildMainNavigation(activeView);
return (
<header className="md-shell-header" data-ui-region="header">
<div className="md-shell-brand">Multi-Debrid-Downloader</div>
<nav aria-label="Hauptnavigation" className="md-shell-navigation" role="tablist">
{navigation.map((item) => (
<button
aria-current={item.active ? "page" : undefined}
aria-selected={item.active}
className={`md-shell-navigation-item${item.active ? " is-active" : ""}`}
data-visual-active-view={item.active ? item.id : undefined}
key={item.id}
onClick={() => onViewChange(item.id)}
role="tab"
title={item.label}
type="button"
>
<Icon name={item.icon} size={18} />
<span>{item.label}</span>
</button>
))}
</nav>
{actions ? <div aria-label="Globale Aktionen" className="md-shell-header-actions" role="group">{actions}</div> : null}
</header>
);
}
+106
View File
@@ -0,0 +1,106 @@
import { useState, useSyncExternalStore, type ReactElement, type ReactNode } from "react";
import { ContextInfoButton } from "../ui/ContextInfoButton";
import { AppHeader } from "./AppHeader";
import { AppSidebar } from "./AppSidebar";
import {
getMainViewLabel,
getResponsiveShellMode,
type MainView,
type ResponsiveShellMode
} from "./shell-model";
import "./shell.css";
function subscribeToViewport(callback: () => void): () => void {
if (typeof window === "undefined") {
return () => undefined;
}
window.addEventListener("resize", callback);
return () => window.removeEventListener("resize", callback);
}
function getViewportShellMode(): ResponsiveShellMode {
return typeof window === "undefined" ? "full" : getResponsiveShellMode(window.innerWidth);
}
function getServerShellMode(): ResponsiveShellMode {
return "full";
}
export interface AppShellProps {
activeView: MainView;
onViewChange: (view: MainView) => void;
sidebar: ReactNode;
sidebarStatus: ReactNode;
headerActions: ReactNode;
toolbar: ReactNode;
children: ReactNode;
footer: ReactNode;
contextInfo: ReactNode;
sidebarCollapsed: boolean;
onSidebarCollapsedChange: (collapsed: boolean) => void;
}
export function AppShell({
activeView,
onViewChange,
sidebar,
sidebarStatus,
headerActions,
toolbar,
children,
footer,
contextInfo,
sidebarCollapsed,
onSidebarCollapsedChange
}: AppShellProps): ReactElement {
const [infoOpen, setInfoOpen] = useState(false);
const [responsiveSidebarExpanded, setResponsiveSidebarExpanded] = useState(false);
const responsiveMode = useSyncExternalStore(
subscribeToViewport,
getViewportShellMode,
getServerShellMode
);
const hasSidebar = Boolean(sidebar || sidebarStatus);
const responsiveSidebarCollapsed = responsiveMode !== "full" && !responsiveSidebarExpanded;
const effectiveSidebarCollapsed = sidebarCollapsed || responsiveSidebarCollapsed;
const setSidebarCollapsed = (collapsed: boolean): void => {
if (responsiveMode !== "full") {
setResponsiveSidebarExpanded(!collapsed);
}
onSidebarCollapsedChange(collapsed);
};
return (
<div
className={`md-shell is-${responsiveMode}${hasSidebar ? " has-sidebar" : ""}${hasSidebar && effectiveSidebarCollapsed ? " has-collapsed-sidebar" : ""}`}
data-responsive-mode={responsiveMode}
>
<AppHeader activeView={activeView} actions={headerActions} onViewChange={onViewChange} />
<div className="md-shell-workspace">
{hasSidebar ? (
<AppSidebar
collapsed={effectiveSidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
responsiveRail={responsiveSidebarCollapsed}
status={sidebarStatus}
>
{sidebar}
</AppSidebar>
) : null}
<section className="md-shell-main" data-ui-region="main">
{toolbar ? <div className="md-shell-toolbar" data-ui-region="toolbar">{toolbar}</div> : null}
<div className="md-shell-content">{children}</div>
{footer ? <div className="md-shell-footer" data-ui-region="footer">{footer}</div> : null}
<ContextInfoButton
content={contextInfo}
contextName={getMainViewLabel(activeView)}
onOpenChange={setInfoOpen}
open={infoOpen}
/>
</section>
</div>
</div>
);
}
+37
View File
@@ -0,0 +1,37 @@
import type { ReactElement, ReactNode } from "react";
import { Icon } from "../ui/Icon";
export interface AppSidebarProps {
children: ReactNode;
status: ReactNode;
collapsed: boolean;
responsiveRail?: boolean;
onCollapsedChange: (collapsed: boolean) => void;
}
export function AppSidebar({
children,
status,
collapsed,
responsiveRail = false,
onCollapsedChange
}: AppSidebarProps): ReactElement {
return (
<aside
className={`md-shell-sidebar${collapsed ? " is-collapsed" : ""}${responsiveRail ? " is-responsive-rail" : ""}`}
data-ui-region="sidebar"
>
{children ? <div className="md-shell-sidebar-scroll">{children}</div> : null}
{status ? <div className="md-shell-sidebar-status" data-ui-region="sidebar-status">{status}</div> : null}
<button
aria-label={collapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
className="md-shell-sidebar-toggle"
onClick={() => onCollapsedChange(!collapsed)}
title={collapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
type="button"
>
<Icon name={collapsed ? "chevron-right" : "chevron-left"} size={14} />
</button>
</aside>
);
}
+119
View File
@@ -0,0 +1,119 @@
import { useEffect, useRef, type ReactElement } from "react";
import { restoreFocus } from "../ui/focus";
export interface AvatarMenuAction {
id: string;
label: string;
danger?: boolean;
onSelect: () => void;
}
export interface AvatarMenuProps {
open: boolean;
accountLabel: string;
actions: AvatarMenuAction[];
onClose: () => void;
}
export type AvatarMenuKeyboardAction =
| { type: "close" }
| { type: "focus"; index: number };
export function getAvatarMenuKeyboardAction(
key: string,
currentIndex: number,
itemCount: number
): AvatarMenuKeyboardAction | null {
if (key === "Escape") {
return { type: "close" };
}
if (itemCount <= 0) {
return null;
}
if (key === "Home") {
return { type: "focus", index: 0 };
}
if (key === "End") {
return { type: "focus", index: itemCount - 1 };
}
if (key === "ArrowDown") {
return { type: "focus", index: (Math.max(currentIndex, -1) + 1) % itemCount };
}
if (key === "ArrowUp") {
return { type: "focus", index: currentIndex <= 0 ? itemCount - 1 : currentIndex - 1 };
}
return null;
}
export function AvatarMenu({ open, accountLabel, actions, onClose }: AvatarMenuProps): ReactElement | null {
const menuRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);
const closeAndRestoreFocus = (): void => {
const trigger = menuRef.current?.parentElement?.querySelector<HTMLButtonElement>('button[aria-haspopup="menu"]');
onClose();
restoreFocus(trigger ?? null);
};
useEffect(() => {
if (!open) {
return;
}
const onPointerDown = (event: MouseEvent): void => {
if (!menuRef.current?.parentElement?.contains(event.target as Node)) {
onClose();
}
};
document.addEventListener("mousedown", onPointerDown);
return () => {
document.removeEventListener("mousedown", onPointerDown);
};
}, [onClose, open]);
if (!open) {
return null;
}
return (
<div
aria-label="Kontomenü"
className="md-avatar-menu"
onKeyDown={(event) => {
const currentIndex = itemRefs.current.findIndex((item) => item === document.activeElement);
const action = getAvatarMenuKeyboardAction(event.key, currentIndex, actions.length);
if (!action) {
return;
}
event.preventDefault();
event.stopPropagation();
if (action.type === "close") {
closeAndRestoreFocus();
return;
}
itemRefs.current[action.index]?.focus();
}}
ref={menuRef}
role="menu"
>
<div className="md-avatar-menu-account">{accountLabel}</div>
{actions.map((action, index) => (
<button
autoFocus={index === 0}
className={`md-avatar-menu-action${action.danger ? " is-danger" : ""}`}
key={action.id}
onClick={() => {
action.onSelect();
closeAndRestoreFocus();
}}
ref={(element) => {
itemRefs.current[index] = element;
}}
role="menuitem"
type="button"
>
{action.label}
</button>
))}
</div>
);
}
+60
View File
@@ -0,0 +1,60 @@
import type { ReactElement, ReactNode } from "react";
export interface OverlayHostProps {
confirm?: ReactNode;
onlineBackup?: ReactNode;
diagnostics?: ReactNode;
deleteConfirmation?: ReactNode;
conflict?: ReactNode;
accountCreate?: ReactNode;
accountEdit?: ReactNode;
keyStats?: ReactNode;
linkPopup?: ReactNode;
update?: ReactNode;
toast?: ReactNode;
accountContextMenu?: ReactNode;
downloadContextMenu?: ReactNode;
columnContextMenu?: ReactNode;
historyContextMenu?: ReactNode;
dropOverlay?: ReactNode;
}
export function OverlayHost({
confirm,
onlineBackup,
diagnostics,
deleteConfirmation,
conflict,
accountCreate,
accountEdit,
keyStats,
linkPopup,
update,
toast,
accountContextMenu,
downloadContextMenu,
columnContextMenu,
historyContextMenu,
dropOverlay
}: OverlayHostProps): ReactElement {
return (
<div className="md-overlay-host" id="md-overlay-host">
{confirm}
{onlineBackup}
{diagnostics}
{deleteConfirmation}
{conflict}
{accountCreate}
{accountEdit}
{keyStats}
{linkPopup}
{update}
{toast}
{accountContextMenu}
{downloadContextMenu}
{columnContextMenu}
{historyContextMenu}
{dropOverlay}
</div>
);
}
+141
View File
@@ -0,0 +1,141 @@
import { useState, type ReactElement } from "react";
import type { UpdateInstallProgress } from "../../shared/types";
import { Dialog } from "../ui/Dialog";
export { getDialogFocusTarget as getUpdateDialogFocusTarget } from "../ui/Dialog";
export type UpdateExperienceState = "prompt" | UpdateInstallProgress["stage"];
export interface UpdateExperienceProgress {
percent: number | null;
text: string;
}
export interface UpdateExperienceProps {
available: boolean;
latestTag: string;
currentVersion: string;
releaseNotes: string;
state: UpdateExperienceState;
progress: UpdateExperienceProgress | number | null;
open: boolean;
onOpen: () => void;
onClose: () => void;
onInstall: () => void;
onLater: () => void;
renderTrigger?: boolean;
renderDialog?: boolean;
}
const activeStates = new Set<UpdateExperienceState>([
"starting",
"downloading",
"verifying",
"launching"
]);
export function UpdateExperience({
available,
latestTag,
currentVersion,
releaseNotes,
state,
progress,
open,
onOpen,
onClose,
onInstall,
onLater,
renderTrigger = true,
renderDialog = true
}: UpdateExperienceProps): ReactElement | null {
const [tooltipOpen, setTooltipOpen] = useState(false);
const progressInfo = typeof progress === "object" ? progress : null;
const active = activeStates.has(state);
const closable = !active;
if (!available && !open) {
return null;
}
return (
<>
{available && renderTrigger ? (
<div
className="md-update-anchor"
onBlur={() => setTooltipOpen(false)}
onFocus={() => setTooltipOpen(true)}
onMouseEnter={() => setTooltipOpen(true)}
onMouseLeave={() => setTooltipOpen(false)}
>
<button
aria-describedby="md-update-tooltip"
aria-label="Update verfügbar"
className="md-update-trigger"
onClick={onOpen}
type="button"
>
Update verfügbar
</button>
<div
aria-label="Update verfügbar"
className={`md-update-tooltip${tooltipOpen ? " is-visible" : ""}`}
id="md-update-tooltip"
role="tooltip"
>
Eine neue Version ist bereit. Klicke hier, um sie zu installieren.
</div>
</div>
) : null}
{renderDialog ? (
<Dialog
actions={state === "prompt" ? (
<>
<button className="btn" onClick={onLater} type="button">Später</button>
<button className="btn primary" onClick={onInstall} type="button">Jetzt aktualisieren</button>
</>
) : null}
actionsClassName="md-update-dialog-actions"
backdropClassName="md-update-backdrop"
bodyClassName="md-update-dialog-body"
className={`md-update-dialog md-update-dialog-${state}`}
closable={closable}
headerClassName="md-update-dialog-header"
onClose={onClose}
open={open}
showCloseButton
size="update"
title="Update installieren"
>
{state === "prompt" ? (
<>
<p>{latestTag} ist verfügbar. Installierte Version: {currentVersion}.</p>
{releaseNotes.trim() ? (
<details className="md-update-release-notes">
<summary>Changelog anzeigen</summary>
<pre>{releaseNotes}</pre>
</details>
) : null}
</>
) : (
<>
<p className="md-update-progress-text">{progressInfo?.text ?? "Update wird vorbereitet..."}</p>
{state === "downloading" && progressInfo?.percent !== null && progressInfo?.percent !== undefined ? (
<div
aria-label="Update-Fortschritt"
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={progressInfo.percent}
className="md-update-progress-track"
role="progressbar"
>
<div className="md-update-progress-fill" style={{ width: `${progressInfo.percent}%` }} />
</div>
) : null}
</>
)}
</Dialog>
) : null}
</>
);
}
+39
View File
@@ -0,0 +1,39 @@
import type { IconName } from "../ui/Icon";
export type MainView = "downloads" | "collector" | "settings" | "history" | "statistics";
export type ResponsiveShellMode = "full" | "compact" | "minimum";
export interface NavigationItem {
id: MainView;
label: string;
icon: IconName;
active: boolean;
}
const MAIN_NAVIGATION = [
{ id: "downloads", label: "Downloads", icon: "download" },
{ id: "collector", label: "Linksammler", icon: "collector" },
{ id: "settings", label: "Einstellungen", icon: "settings" },
{ id: "history", label: "Verlauf", icon: "history" },
{ id: "statistics", label: "Statistiken", icon: "statistics" }
] as const;
export function buildMainNavigation(active: MainView): NavigationItem[] {
return MAIN_NAVIGATION.map((item) => ({ ...item, active: item.id === active }));
}
export function getMainViewLabel(view: MainView): string {
return MAIN_NAVIGATION.find((item) => item.id === view)?.label ?? view;
}
export function getResponsiveShellMode(width: number): ResponsiveShellMode {
if (width <= 1120) {
return "minimum";
}
if (width <= 1366) {
return "compact";
}
return "full";
}
+822
View File
@@ -0,0 +1,822 @@
.md-runtime-root {
--md-layer-avatar: 500;
--md-layer-menu: 600;
--md-layer-tooltip: 700;
--md-layer-toast: 800;
--md-layer-modal: 1000;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.md-runtime-root.settings-active {
user-select: none;
}
.md-runtime-root.settings-active input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
.md-runtime-root.settings-active textarea,
.md-runtime-root.settings-active [contenteditable]:not([contenteditable="false"]) {
user-select: text;
}
.md-shell {
display: grid;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
grid-template-rows: 40px minmax(0, 1fr);
gap: 8px;
padding: 8px 16px 16px;
overflow: hidden;
background: var(--ui-canvas);
color: var(--ui-text);
}
.md-shell-header {
position: relative;
display: flex;
min-width: 0;
height: 40px;
align-items: center;
gap: 12px;
border: 1px solid var(--ui-border);
border-radius: 8px;
background: var(--ui-surface);
}
.md-shell-brand {
flex: 0 0 auto;
padding: 0 16px;
color: var(--ui-text);
font-size: 16px;
font-weight: 700;
line-height: 20px;
}
.md-shell-navigation {
display: flex;
min-width: 0;
flex: 1 1 auto;
height: 100%;
align-items: center;
gap: 2px;
overflow: hidden;
}
.md-shell-navigation-item {
display: inline-flex;
height: 32px;
padding: 0 10px;
align-items: center;
gap: 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--ui-text-secondary);
cursor: pointer;
font-weight: 600;
}
.md-shell-navigation-item:hover {
background: var(--ui-hover);
color: var(--ui-text);
}
.md-shell-navigation-item.is-active {
background: var(--ui-active);
color: var(--ui-text);
}
.md-shell-navigation-item:focus-visible,
.md-shell-header-actions button:focus-visible,
.md-shell-sidebar-toggle:focus-visible,
.md-avatar-menu-action:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: 2px;
}
.md-shell-header-actions {
position: relative;
display: flex;
min-width: 0;
flex: 0 0 auto;
height: 100%;
margin-left: auto;
align-items: center;
gap: 4px;
padding-right: 6px;
}
.md-update-anchor {
position: relative;
display: flex;
height: 100%;
align-items: center;
}
.md-update-trigger {
display: inline-flex;
height: 40px;
min-width: 90px;
padding: 0 14px;
align-items: center;
justify-content: center;
border: 0;
border-radius: 6px;
background: var(--ui-primary);
color: #0f0f0f;
cursor: pointer;
font-size: 14px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
}
.md-update-trigger:hover {
background: var(--ui-primary-hover);
}
.md-update-trigger:focus-visible,
.md-update-dialog button:focus-visible,
.md-update-release-notes summary:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: 2px;
}
.md-update-tooltip {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: var(--md-layer-tooltip);
display: flex;
width: 200px;
min-height: 68px;
padding: 12px;
align-items: center;
box-sizing: border-box;
border-radius: 8px;
background: #4f4d4d;
box-shadow: 0 4px 12px rgb(0 0 0 / 35%);
color: #fff;
font-size: 12px;
line-height: 16px;
opacity: 0;
pointer-events: none;
visibility: hidden;
}
.md-update-anchor:hover .md-update-tooltip,
.md-update-anchor:focus-within .md-update-tooltip,
.md-update-tooltip.is-visible {
opacity: 1;
visibility: visible;
}
.md-update-backdrop {
position: fixed;
inset: 0;
z-index: 1300;
display: grid;
padding: 16px;
place-items: center;
background: rgb(0 0 0 / 60%);
}
.md-update-dialog {
display: grid;
width: min(548px, calc(100vw - 32px));
min-height: 207px;
max-height: calc(100vh - 32px);
grid-template-rows: 56px minmax(92px, auto) 58px;
overflow: auto;
transform: translateY(-24px);
border: 1px solid var(--ui-border);
border-radius: 16px;
background: var(--ui-surface);
box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
color: var(--ui-text);
}
.md-update-dialog:not(.md-update-dialog-prompt) {
grid-template-rows: 56px minmax(150px, auto);
}
.md-update-dialog:focus {
outline: none;
}
.md-update-dialog-header {
display: flex;
min-width: 0;
height: 56px;
padding: 0 16px 0 20px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--ui-border);
}
.md-update-dialog-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
.md-update-dialog-close {
display: grid;
width: 32px;
height: 32px;
padding: 0;
place-items: center;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--ui-text-secondary);
cursor: pointer;
font-size: 24px;
line-height: 24px;
}
.md-update-dialog-close:hover {
background: var(--ui-hover);
color: var(--ui-text);
}
.md-update-dialog-body {
min-width: 0;
padding: 16px 20px;
overflow: auto;
}
.md-update-dialog-body p {
margin: 0;
color: var(--ui-text-secondary);
line-height: 20px;
}
.md-update-release-notes {
margin-top: 10px;
color: var(--ui-text-secondary);
}
.md-update-release-notes summary {
width: max-content;
cursor: pointer;
font-size: 12px;
font-weight: 600;
line-height: 16px;
}
.md-update-release-notes pre {
margin: 10px 0 0;
color: var(--ui-text-secondary);
font: inherit;
line-height: 20px;
white-space: pre-wrap;
}
.md-update-dialog-actions {
display: flex;
min-height: 58px;
padding: 10px 16px;
align-items: center;
justify-content: flex-end;
gap: 8px;
border-top: 1px solid var(--ui-border);
}
.md-update-progress-text {
min-height: 20px;
}
.md-update-progress-track {
height: 8px;
margin-top: 16px;
overflow: hidden;
border-radius: 999px;
background: var(--ui-input);
}
.md-update-progress-fill {
height: 100%;
border-radius: inherit;
background: var(--ui-accent);
}
.md-application-menu-tree {
display: flex;
height: 100%;
align-items: center;
user-select: none;
}
.md-application-menu-tree .menu-bar-trigger {
height: 32px;
padding: 0 8px;
}
.md-download-sidebar-content {
display: grid;
gap: 12px;
}
.md-download-sidebar-search {
display: grid;
gap: 4px;
color: var(--ui-text-secondary);
font-size: 12px;
font-weight: 600;
line-height: 16px;
}
.md-download-sidebar-search input {
width: 100%;
}
.md-download-sidebar-actions {
display: grid;
gap: 4px;
}
.md-download-sidebar-actions .btn {
width: 100%;
text-align: left;
}
.md-runtime-view-content {
display: flex;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.md-runtime-view-content > * {
width: 100%;
min-width: 0;
min-height: 0;
}
.md-shell-workspace {
display: grid;
min-width: 0;
min-height: 0;
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.md-shell.has-sidebar .md-shell-workspace {
grid-template-columns: 270px minmax(0, 1fr);
}
.md-shell.has-collapsed-sidebar .md-shell-workspace {
grid-template-columns: 56px minmax(0, 1fr);
}
.md-shell-sidebar,
.md-shell-main {
position: relative;
min-width: 0;
min-height: 0;
border: 1px solid var(--ui-border);
border-radius: 8px;
background: var(--ui-surface);
}
.md-shell-sidebar {
display: flex;
flex-direction: column;
}
.md-shell-sidebar-scroll {
min-height: 0;
flex: 1 1 auto;
overflow: auto;
padding: 12px;
}
.md-shell-sidebar-status {
display: grid;
flex: 0 0 auto;
gap: 4px;
padding: 12px;
border-top: 1px solid var(--ui-border);
color: var(--ui-text-secondary);
font-size: 12px;
line-height: 16px;
}
.md-shell-sidebar.is-collapsed :where(.md-shell-sidebar-scroll, .md-shell-sidebar-status) {
overflow: hidden;
visibility: hidden;
}
.md-shell-sidebar-toggle {
position: absolute;
top: 50%;
right: -8px;
z-index: 5;
display: grid;
width: 14px;
height: 42px;
padding: 0;
place-items: center;
transform: translateY(-50%);
border: 1px solid var(--ui-border);
border-radius: 4px;
background: var(--ui-input);
color: var(--ui-text-muted);
cursor: pointer;
opacity: 0;
}
.md-shell-sidebar.is-responsive-rail .md-shell-sidebar-toggle {
top: 12px;
right: auto;
left: 11px;
width: 32px;
height: 32px;
transform: none;
opacity: 1;
}
.md-shell-sidebar:hover .md-shell-sidebar-toggle,
.md-shell-sidebar-toggle:focus-visible {
opacity: 1;
}
.md-shell-main {
display: flex;
overflow: hidden;
flex-direction: column;
}
.md-shell-toolbar {
min-width: 0;
flex: 0 0 auto;
overflow-x: auto;
overflow-y: hidden;
}
.md-shell-content {
min-width: 0;
min-height: 0;
flex: 1 1 auto;
overflow: hidden;
}
.md-shell-footer {
flex: 0 0 auto;
border-top: 1px solid var(--ui-border);
}
.md-avatar-anchor {
position: relative;
}
.md-avatar-trigger {
display: grid;
width: 32px;
height: 32px;
padding: 0;
place-items: center;
border: 0;
border-radius: 50%;
background: var(--ui-active);
color: var(--ui-text-secondary);
cursor: pointer;
}
.md-avatar-menu {
position: absolute;
top: calc(100% + 5px);
right: 0;
z-index: var(--md-layer-avatar);
display: flex;
width: 174px;
min-height: 106px;
padding: 8px;
flex-direction: column;
border: 1px solid var(--ui-border);
border-radius: 8px;
background: var(--ui-surface);
}
.md-avatar-menu-account {
overflow: hidden;
padding: 4px 8px 8px;
color: var(--ui-text-muted);
font-size: 12px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.md-avatar-menu-action {
min-height: 30px;
padding: 5px 8px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--ui-text-secondary);
cursor: pointer;
text-align: left;
}
.md-avatar-menu-action:hover {
background: var(--ui-hover);
color: var(--ui-text);
}
.md-avatar-menu-action.is-danger {
color: var(--ui-danger);
}
.md-application-menu-tree :where(.menu-dropdown, .menu-submenu-dropdown) {
z-index: var(--md-layer-menu);
}
.md-overlay-host {
position: static;
}
.md-dialog-backdrop {
position: fixed;
inset: 0;
z-index: var(--md-layer-modal);
display: grid;
padding: 20px;
place-items: center;
background: var(--ui-overlay);
}
.md-dialog {
display: grid;
width: min(560px, calc(100vw - 40px));
max-height: calc(100vh - 40px);
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 0;
overflow: hidden;
padding: 0;
border: 1px solid var(--ui-border);
border-radius: 16px;
outline: none;
background: var(--ui-surface);
box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
color: var(--ui-text);
}
.md-dialog.md-dialog-size-account {
width: min(660px, calc(100vw - 40px));
}
.md-dialog.md-dialog-size-wide {
width: min(760px, calc(100vw - 40px));
}
.md-dialog.md-dialog-size-update {
width: min(548px, calc(100vw - 32px));
}
.md-dialog-header {
display: flex;
min-width: 0;
min-height: 56px;
padding: 12px 16px 12px 20px;
align-items: center;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--ui-border);
}
.md-dialog-header h2 {
margin: 0;
color: var(--ui-text);
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
.md-dialog-close {
display: grid;
width: 32px;
height: 32px;
flex: 0 0 auto;
padding: 0;
place-items: center;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--ui-text-secondary);
cursor: pointer;
font-size: 24px;
line-height: 24px;
}
.md-dialog-close:hover {
background: var(--ui-hover);
color: var(--ui-text);
}
.md-dialog-body {
min-width: 0;
min-height: 0;
overflow: auto;
padding: 16px 20px;
}
.md-dialog-body > :first-child {
margin-top: 0;
}
.md-dialog-body > :last-child {
margin-bottom: 0;
}
.md-dialog-description,
.md-dialog-body p {
color: var(--ui-text-secondary);
}
.md-dialog-actions {
min-height: 58px;
padding: 10px 16px;
border-top: 1px solid var(--ui-border);
background: var(--ui-surface);
}
.md-dialog :where(button, summary, input, select, textarea):focus-visible,
.md-context-menu [role="menuitem"]:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: 2px;
}
.md-context-menu {
z-index: var(--md-layer-menu);
max-width: calc(100vw - 8px);
max-height: calc(100vh - 8px);
overflow: auto;
border-color: var(--ui-border);
background: var(--ui-surface);
box-shadow: 0 4px 12px rgb(0 0 0 / 35%);
color: var(--ui-text);
}
.md-context-menu .ctx-menu-item {
color: var(--ui-text);
}
.md-context-menu .ctx-menu-item:hover,
.md-context-menu .ctx-menu-item:focus-visible {
background: var(--ui-hover);
}
.md-context-menu .ctx-menu-item.ctx-danger {
color: var(--ui-danger);
}
.md-context-menu .ctx-menu-sep {
background: var(--ui-border);
}
.md-context-menu .ctx-menu-sub.is-keyboard-open > .ctx-menu-sub-items {
display: block;
}
.md-toast {
right: 20px;
bottom: 84px;
z-index: var(--md-layer-toast);
max-width: min(420px, calc(100vw - 40px));
border-color: var(--ui-border);
background: var(--ui-surface);
color: var(--ui-text);
box-shadow: 0 4px 12px rgb(0 0 0 / 35%);
}
.md-drop-overlay {
z-index: var(--md-layer-modal);
pointer-events: none;
background: var(--ui-overlay);
border-color: color-mix(in srgb, var(--ui-accent) 72%, transparent);
color: var(--ui-text);
}
.md-overlay-host .md-dialog-backdrop {
z-index: var(--md-layer-modal);
padding: 20px;
background: var(--ui-overlay);
}
.md-overlay-host .md-dialog {
gap: 0;
overflow: hidden;
padding: 0;
border-color: var(--ui-border);
border-radius: 16px;
background: var(--ui-surface);
box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
color: var(--ui-text);
}
.md-overlay-host .md-context-menu {
z-index: var(--md-layer-menu);
border-color: var(--ui-border);
background: var(--ui-surface);
box-shadow: 0 4px 12px rgb(0 0 0 / 35%);
color: var(--ui-text);
}
.md-overlay-host .md-toast {
bottom: 84px;
z-index: var(--md-layer-toast);
border-color: var(--ui-border);
background: var(--ui-surface);
box-shadow: 0 4px 12px rgb(0 0 0 / 35%);
color: var(--ui-text);
}
.md-overlay-host .md-drop-overlay {
z-index: var(--md-layer-modal);
pointer-events: none;
background: var(--ui-overlay);
color: var(--ui-text);
}
@media (max-width: 1366px) {
.md-shell.is-compact .md-shell-navigation-item span,
.md-shell.is-compact .md-shell-brand,
.md-shell.is-minimum .md-shell-navigation-item span,
.md-shell.is-minimum .md-shell-brand {
display: none;
}
.md-shell.is-compact .md-shell-header,
.md-shell.is-minimum .md-shell-header {
gap: 6px;
}
.md-shell.is-compact .md-shell-navigation,
.md-shell.is-minimum .md-shell-navigation {
flex: 1 1 auto;
overflow: hidden;
}
.md-shell.is-compact .md-shell-navigation-item,
.md-shell.is-minimum .md-shell-navigation-item {
width: 36px;
flex: 0 0 36px;
padding-inline: 9px;
justify-content: center;
}
.md-shell.is-compact .md-shell-header-actions,
.md-shell.is-minimum .md-shell-header-actions {
flex: 0 0 auto;
}
}
@media (max-width: 1120px) {
.md-shell.is-minimum {
gap: 6px;
padding: 6px 8px 8px;
}
.md-shell.is-minimum .md-shell-workspace {
gap: 6px;
}
.md-dialog-backdrop,
.md-overlay-host .md-dialog-backdrop,
.md-update-backdrop {
padding: 12px;
}
.md-dialog,
.md-dialog.md-dialog-size-account,
.md-dialog.md-dialog-size-wide,
.md-dialog.md-dialog-size-update,
.md-update-dialog {
max-width: 100%;
max-height: calc(100vh - 24px);
}
.md-dialog-actions,
.md-update-dialog-actions {
flex-wrap: wrap;
}
}
@media (prefers-reduced-motion: reduce) {
.md-shell *,
.md-shell *::before,
.md-shell *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}