CollapsibileUI (#1231)
* refactor:collapsibleUI Add surfaceClass API / rewrite collapsible * fix: satisfy collapsibleUI stylelint * ok papi * Update CollapsibleUI * Updates * bleh * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
d3410ee8e7
commit
1a99148ee2
6 changed files with 1325 additions and 0 deletions
210
src/api/SurfaceClasses.ts
Normal file
210
src/api/SurfaceClasses.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { useForceUpdater } from "@utils/react";
|
||||
import { useEffect } from "@webpack/common";
|
||||
import type { CSSProperties, FocusEventHandler, MouseEventHandler, RefCallback } from "react";
|
||||
|
||||
/**
|
||||
* Adds plugin-owned semantic data attributes and a limited set of props to
|
||||
* stable Discord-owned layout surfaces, so plugins can style/track layout
|
||||
* state without patching compiled Discord JSX directly.
|
||||
*
|
||||
* This is not a generic prop bus and not for plugin-local components. It
|
||||
* intentionally does not expose className: injected props land before or
|
||||
* after Discord's own props depending on the surface, so class merging
|
||||
* cannot be guaranteed. Use data attributes as CSS hooks instead.
|
||||
*/
|
||||
|
||||
export type SurfaceId =
|
||||
| "base"
|
||||
| "sidebar"
|
||||
| "guildBar"
|
||||
| "channelList"
|
||||
| "membersList"
|
||||
| "titleBar"
|
||||
| "headerBar"
|
||||
| "userArea";
|
||||
|
||||
export interface SurfaceProvidedProps {
|
||||
ref?: RefCallback<HTMLElement>;
|
||||
style?: CSSProperties;
|
||||
[dataAttribute: `data-${string}`]: string | undefined;
|
||||
onFocusCapture?: FocusEventHandler<HTMLElement>;
|
||||
onBlurCapture?: FocusEventHandler<HTMLElement>;
|
||||
onMouseDownCapture?: MouseEventHandler<HTMLElement>;
|
||||
onMouseOverCapture?: MouseEventHandler<HTMLElement>;
|
||||
onMouseOutCapture?: MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
export type SurfacePropsProvider = () => SurfaceProvidedProps | undefined;
|
||||
|
||||
interface SurfaceInstance {
|
||||
forceUpdate(callback?: () => void): void;
|
||||
}
|
||||
|
||||
const propsProviders = new Map<SurfaceId, Set<SurfacePropsProvider>>();
|
||||
const listeners = new Map<SurfaceId, Set<() => void>>();
|
||||
const surfaceInstances = new Map<SurfaceId, WeakRef<SurfaceInstance>>();
|
||||
const failedPropsProviders = new WeakSet<SurfacePropsProvider>();
|
||||
const logger = new Logger("SurfaceClasses");
|
||||
|
||||
function getPropsProviderSet(surfaceId: SurfaceId) {
|
||||
let set = propsProviders.get(surfaceId);
|
||||
|
||||
if (set == null) {
|
||||
set = new Set();
|
||||
propsProviders.set(surfaceId, set);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
function getListenerSet(surfaceId: SurfaceId) {
|
||||
let set = listeners.get(surfaceId);
|
||||
|
||||
if (set == null) {
|
||||
set = new Set();
|
||||
listeners.set(surfaceId, set);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
function chainHandlers<E>(
|
||||
first?: (event: E) => void,
|
||||
second?: (event: E) => void
|
||||
) {
|
||||
if (!first) return second;
|
||||
if (!second) return first;
|
||||
|
||||
return (event: E) => {
|
||||
first(event);
|
||||
second(event);
|
||||
};
|
||||
}
|
||||
|
||||
function chainRefs<T>(first?: RefCallback<T>, second?: RefCallback<T>) {
|
||||
if (!first) return second;
|
||||
if (!second) return first;
|
||||
|
||||
return (instance: T | null) => {
|
||||
first(instance);
|
||||
second(instance);
|
||||
};
|
||||
}
|
||||
|
||||
function mergeSurfaceProvidedProps(target: SurfaceProvidedProps, source: SurfaceProvidedProps) {
|
||||
const ref = chainRefs(target.ref, source.ref);
|
||||
if (ref) target.ref = ref;
|
||||
const onFocusCapture = chainHandlers(target.onFocusCapture, source.onFocusCapture);
|
||||
if (onFocusCapture) target.onFocusCapture = onFocusCapture;
|
||||
const onBlurCapture = chainHandlers(target.onBlurCapture, source.onBlurCapture);
|
||||
if (onBlurCapture) target.onBlurCapture = onBlurCapture;
|
||||
const onMouseDownCapture = chainHandlers(target.onMouseDownCapture, source.onMouseDownCapture);
|
||||
if (onMouseDownCapture) target.onMouseDownCapture = onMouseDownCapture;
|
||||
const onMouseOverCapture = chainHandlers(target.onMouseOverCapture, source.onMouseOverCapture);
|
||||
if (onMouseOverCapture) target.onMouseOverCapture = onMouseOverCapture;
|
||||
const onMouseOutCapture = chainHandlers(target.onMouseOutCapture, source.onMouseOutCapture);
|
||||
if (onMouseOutCapture) target.onMouseOutCapture = onMouseOutCapture;
|
||||
|
||||
if (source.style) {
|
||||
target.style = { ...target.style, ...source.style };
|
||||
}
|
||||
|
||||
// Copy only string data-* attributes so providers cannot smuggle in
|
||||
// arbitrary props (className in particular is deliberately unsupported).
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (key.startsWith("data-") && typeof value === "string") {
|
||||
target[key as `data-${string}`] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
function getSurfaceProps(surfaceId: SurfaceId) {
|
||||
const props: SurfaceProvidedProps = {};
|
||||
|
||||
for (const provider of propsProviders.get(surfaceId) ?? []) {
|
||||
let providedProps: SurfaceProvidedProps | undefined;
|
||||
|
||||
try {
|
||||
providedProps = provider();
|
||||
} catch (error) {
|
||||
if (!failedPropsProviders.has(provider)) {
|
||||
failedPropsProviders.add(provider);
|
||||
logger.error(`Surface props provider failed for ${surfaceId}`, error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (providedProps) {
|
||||
mergeSurfaceProvidedProps(props, providedProps);
|
||||
}
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function notifyOneSurface(surfaceId: SurfaceId) {
|
||||
const surfaceInstance = surfaceInstances.get(surfaceId)?.deref();
|
||||
if (surfaceInstance) {
|
||||
surfaceInstance.forceUpdate();
|
||||
} else {
|
||||
surfaceInstances.delete(surfaceId);
|
||||
}
|
||||
|
||||
for (const listener of listeners.get(surfaceId) ?? []) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
export function addSurfacePropsProvider(surfaceId: SurfaceId, provider: SurfacePropsProvider) {
|
||||
getPropsProviderSet(surfaceId).add(provider);
|
||||
notifyOneSurface(surfaceId);
|
||||
|
||||
return () => {
|
||||
propsProviders.get(surfaceId)?.delete(provider);
|
||||
notifyOneSurface(surfaceId);
|
||||
};
|
||||
}
|
||||
|
||||
/** Re-renders a surface after the state backing one of its providers changed. */
|
||||
export function notifySurfaceClassesChanged(surfaceId: SurfaceId) {
|
||||
notifyOneSurface(surfaceId);
|
||||
}
|
||||
|
||||
/** @internal Injected by SurfaceClassesAPI patch (do NOT call directly) */
|
||||
export function _getSurfaceProps(surfaceId: SurfaceId) {
|
||||
return getSurfaceProps(surfaceId);
|
||||
}
|
||||
|
||||
/** @internal Injected by SurfaceClassesAPI patch (do NOT call directly) */
|
||||
export function _useSurfaceProps(surfaceId: SurfaceId) {
|
||||
const forceUpdate = useForceUpdater();
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => forceUpdate();
|
||||
|
||||
getListenerSet(surfaceId).add(listener);
|
||||
return () => { listeners.get(surfaceId)?.delete(listener); };
|
||||
}, [surfaceId]);
|
||||
|
||||
return getSurfaceProps(surfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Injected by SurfaceClassesAPI patch (do NOT call directly).
|
||||
* Only used for class component surfaces where hooks cannot be injected into render().
|
||||
* Function component surfaces use _useSurfaceProps instead.
|
||||
*/
|
||||
export function _trackSurfaceInstance(surfaceId: SurfaceId, instance: SurfaceInstance) {
|
||||
if (surfaceInstances.get(surfaceId)?.deref() !== instance) {
|
||||
surfaceInstances.set(surfaceId, new WeakRef(instance));
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ import * as $ProfileSections from "./ProfileSections";
|
|||
import * as $ServerList from "./ServerList";
|
||||
import * as $Settings from "./Settings";
|
||||
import * as $Styles from "./Styles";
|
||||
import * as $SurfaceClasses from "./SurfaceClasses";
|
||||
import * as $Themes from "./Themes";
|
||||
import * as $UserSettings from "./UserSettings";
|
||||
|
||||
|
|
@ -98,6 +99,12 @@ export const Badges = $Badges;
|
|||
*/
|
||||
export const ServerList = $ServerList;
|
||||
|
||||
/**
|
||||
* An API allowing plugins to add semantic data attributes and limited props
|
||||
* to stable Discord layout surfaces without patching them directly.
|
||||
*/
|
||||
export const SurfaceClasses = $SurfaceClasses;
|
||||
|
||||
/**
|
||||
* An API allowing you to add components as message accessories.
|
||||
*/
|
||||
|
|
|
|||
68
src/equicordplugins/_api/surfaceClasses.ts
Normal file
68
src/equicordplugins/_api/surfaceClasses.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "SurfaceClassesAPI",
|
||||
description: "API to add plugin-owned semantic data attributes and limited props to stable Discord layout surfaces.",
|
||||
authors: [EquicordDevs.benjii],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "AnnouncementsSpoilerIcon:",
|
||||
replacement: [
|
||||
{
|
||||
match: /"data-fullscreen":\i.{0,150},\{isSidebarOpen:/,
|
||||
replace: '...Vencord.Api.SurfaceClasses._useSurfaceProps("base"),$&'
|
||||
},
|
||||
{
|
||||
match: /"data-collapsed":\i,.{0,130}themeOverride/,
|
||||
replace: '...Vencord.Api.SurfaceClasses._useSurfaceProps("sidebar"),$&'
|
||||
},
|
||||
{
|
||||
match: /\.CHANNEL_SIDEBAR_RESIZED,\{.{0,100}\i=\{/,
|
||||
replace: '$&...Vencord.Api.SurfaceClasses._useSurfaceProps("channelList"),'
|
||||
},
|
||||
{
|
||||
match: /ref:\i.{0,70}#{intl::vTl6Lk::raw}\),/,
|
||||
replace: "...Vencord.Api.SurfaceClasses._useSurfaceProps('userArea'),$&"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "#{intl::GUILDS_BAR_A11Y_LABEL}",
|
||||
replacement: [
|
||||
{
|
||||
match: /"aria-label":.{0,50}#{intl::GUILDS_BAR_A11Y_LABEL}\),children:/,
|
||||
replace: '...Vencord.Api.SurfaceClasses._useSurfaceProps("guildBar"),$&'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "#{intl::MEMBERS_LIST_LANDMARK_LABEL}",
|
||||
replacement: {
|
||||
match: /"aria-labelledby":.{0,130}#{intl::MEMBERS_LIST_LANDMARK_LABEL}/,
|
||||
replace: '...(Vencord.Api.SurfaceClasses._trackSurfaceInstance("membersList",this),Vencord.Api.SurfaceClasses._getSurfaceProps("membersList")),$&'
|
||||
}
|
||||
},
|
||||
{
|
||||
find: '?"refresh-title-bar-small":',
|
||||
replacement: {
|
||||
match: /"data-window-chrome":"true"/,
|
||||
replace: '...Vencord.Api.SurfaceClasses._useSurfaceProps("headerBar"),$&'
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".Masks.HEADER_BAR_BADGE_TOP:",
|
||||
replacement: {
|
||||
match: /"aria-label":\i.{0,25}role:\i,ref:\i/,
|
||||
replace: '...Vencord.Api.SurfaceClasses._useSurfaceProps("titleBar"),$&'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
542
src/equicordplugins/collapsibleUi/index.tsx
Normal file
542
src/equicordplugins/collapsibleUi/index.tsx
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ChannelToolbarButton } from "@api/HeaderBar";
|
||||
import { addSurfacePropsProvider, notifySurfaceClassesChanged, type SurfaceId, type SurfaceProvidedProps } from "@api/SurfaceClasses";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Clickable, ContextMenuApi, Menu } from "@webpack/common";
|
||||
import type { FocusEvent as ReactFocusEvent, MouseEvent as ReactMouseEvent, ReactNode, SVGProps } from "react";
|
||||
|
||||
import { collapseSettingKeys, PanelId, panelRegistry, setCollapseSettingChangeHandler, settings, setUserAreaDetachSettingChangeHandler, toolbarPanelOrder } from "./settings";
|
||||
import managedStyle from "./style.css?managed";
|
||||
|
||||
const cl = classNameFactory("vc-collapsible-ui-");
|
||||
|
||||
const panelDependentSurfaces: Record<PanelId, SurfaceId[]> = {
|
||||
guildBar: ["guildBar", "userArea"],
|
||||
channelList: ["channelList", "base", "sidebar", "userArea"],
|
||||
membersList: ["membersList"],
|
||||
chatButtons: [],
|
||||
titleBar: ["titleBar"],
|
||||
headerBar: ["headerBar", "base"],
|
||||
userArea: ["userArea", "sidebar"],
|
||||
};
|
||||
|
||||
const DETACHED_USER_AREA_WIDTH = 312;
|
||||
const DETACHED_USER_AREA_HEIGHT = 88;
|
||||
const DETACHED_USER_AREA_MARGIN = 16;
|
||||
const DETACHED_USER_AREA_DEFAULT_OFFSET_X = 24;
|
||||
const DETACHED_USER_AREA_DEFAULT_OFFSET_Y = 88;
|
||||
|
||||
let providerUnsubs: Array<() => void> = [];
|
||||
let channelListExpandedByInteraction = false;
|
||||
let headerBarExpandedByInteraction = false;
|
||||
let headerBarPointerTrackerEnabled = false;
|
||||
let userAreaDragState: { offsetX: number; offsetY: number; width: number; height: number; } | undefined;
|
||||
let detachedUserAreaDragPosition: { x: number; y: number; } | undefined;
|
||||
let detachedUserAreaPositionChanged = false;
|
||||
let detachedUserAreaAnimationFrame: number | undefined;
|
||||
let channelListElement: HTMLElement | null = null;
|
||||
let userAreaElement: HTMLElement | null = null;
|
||||
const surfaceCssPxCache = new Map<string, number>();
|
||||
|
||||
function PanelsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path fill="currentColor" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3H3V5Zm0 5h6v11H5a2 2 0 0 1-2-2V10Zm8 0h10v9a2 2 0 0 1-2 2H11V10Zm2-5h8v3h-8V5Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function isPanelCollapsed(panelId: PanelId) {
|
||||
return settings.plain[panelRegistry[panelId].collapsedKey];
|
||||
}
|
||||
|
||||
function usePanelCollapsed(panelId: PanelId) {
|
||||
const key = panelRegistry[panelId].collapsedKey;
|
||||
return settings.use([key])[key];
|
||||
}
|
||||
|
||||
function notifyPanelSurfacesChanged(panelId: PanelId) {
|
||||
for (const surfaceId of panelDependentSurfaces[panelId]) {
|
||||
notifySurfaceClassesChanged(surfaceId);
|
||||
}
|
||||
}
|
||||
|
||||
function setHeaderBarExpandedByInteraction(expanded: boolean) {
|
||||
if (headerBarExpandedByInteraction === expanded) return;
|
||||
headerBarExpandedByInteraction = expanded;
|
||||
notifySurfaceClassesChanged("base");
|
||||
notifySurfaceClassesChanged("headerBar");
|
||||
}
|
||||
|
||||
function setChannelListExpandedByInteraction(expanded: boolean) {
|
||||
if (channelListExpandedByInteraction === expanded) return;
|
||||
channelListExpandedByInteraction = expanded;
|
||||
notifySurfaceClassesChanged("base");
|
||||
notifySurfaceClassesChanged("sidebar");
|
||||
}
|
||||
|
||||
function syncPanelCollapsedState(panelId: PanelId, collapsed: boolean) {
|
||||
if (panelId === "channelList") {
|
||||
channelListExpandedByInteraction = false;
|
||||
}
|
||||
|
||||
if (panelId === "headerBar") {
|
||||
setHeaderBarPointerTrackerEnabled(collapsed);
|
||||
if (!collapsed) {
|
||||
headerBarExpandedByInteraction = false;
|
||||
}
|
||||
}
|
||||
|
||||
notifyPanelSurfacesChanged(panelId);
|
||||
}
|
||||
|
||||
function syncAllPanelCollapsedStates() {
|
||||
for (const panelId of toolbarPanelOrder) {
|
||||
syncPanelCollapsedState(panelId, isPanelCollapsed(panelId));
|
||||
}
|
||||
}
|
||||
|
||||
// Reads a plugin CSS length variable from a mounted surface so the managed
|
||||
// stylesheet stays the single source of truth. Cached while the pointer
|
||||
// tracker runs; cleared each time tracking starts so theme overrides of the
|
||||
// variables are picked up without querying computed style per mouse move.
|
||||
function readSurfacePx(property: string, fallback: number) {
|
||||
const cached = surfaceCssPxCache.get(property);
|
||||
if (cached != null) return cached;
|
||||
|
||||
const host = userAreaElement ?? channelListElement;
|
||||
if (!host) return fallback;
|
||||
|
||||
const value = parseFloat(getComputedStyle(host).getPropertyValue(property));
|
||||
if (!Number.isFinite(value)) return fallback;
|
||||
|
||||
surfaceCssPxCache.set(property, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Electron drag regions do not provide stable hover events, so keep this as a
|
||||
// coordinate-only tracker while headerbar collapse is enabled.
|
||||
function handleHeaderBarPointerMove(event: MouseEvent) {
|
||||
if (!isPanelCollapsed("headerBar")) {
|
||||
setHeaderBarPointerTrackerEnabled(false);
|
||||
setHeaderBarExpandedByInteraction(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const interactionHeight = headerBarExpandedByInteraction
|
||||
? readSurfacePx("--vc-cui-header-bar-height", 32)
|
||||
: readSurfacePx("--vc-cui-collapsed-block-size", 8);
|
||||
setHeaderBarExpandedByInteraction(event.clientY >= 0 && event.clientY <= interactionHeight);
|
||||
}
|
||||
|
||||
function setHeaderBarPointerTrackerEnabled(enabled: boolean) {
|
||||
if (headerBarPointerTrackerEnabled === enabled) return;
|
||||
headerBarPointerTrackerEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
surfaceCssPxCache.clear();
|
||||
document.addEventListener("mousemove", handleHeaderBarPointerMove, true);
|
||||
} else {
|
||||
document.removeEventListener("mousemove", handleHeaderBarPointerMove, true);
|
||||
}
|
||||
}
|
||||
|
||||
function setPanelCollapsed(panelId: PanelId, collapsed: boolean) {
|
||||
const key = panelRegistry[panelId].collapsedKey;
|
||||
if (settings.plain[key] === collapsed) return;
|
||||
settings.store[key] = collapsed;
|
||||
}
|
||||
|
||||
function togglePanel(panelId: PanelId) {
|
||||
setPanelCollapsed(panelId, !isPanelCollapsed(panelId));
|
||||
}
|
||||
|
||||
function openToolbarMenu(event: ReactMouseEvent) {
|
||||
ContextMenuApi.openContextMenu(event, () => <ToolbarMenu onClose={ContextMenuApi.closeContextMenu} />);
|
||||
}
|
||||
|
||||
function containsRelatedTarget(event: ReactFocusEvent<HTMLElement> | ReactMouseEvent<HTMLElement>) {
|
||||
const { currentTarget, relatedTarget } = event;
|
||||
return relatedTarget instanceof Node && currentTarget.contains(relatedTarget);
|
||||
}
|
||||
|
||||
function nodeWithin(element: HTMLElement | null, node: unknown) {
|
||||
return node instanceof Node && !!element?.contains(node);
|
||||
}
|
||||
|
||||
function eventWithin(element: HTMLElement | null, event: ReactFocusEvent<HTMLElement> | ReactMouseEvent<HTMLElement>) {
|
||||
if (!element) return false;
|
||||
|
||||
if (nodeWithin(element, event.target)) return true;
|
||||
if (event.nativeEvent.composedPath().some(node => nodeWithin(element, node))) return true;
|
||||
|
||||
if ("clientX" in event && "clientY" in event) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleSidebarEnter(event: ReactFocusEvent<HTMLElement> | ReactMouseEvent<HTMLElement>) {
|
||||
if (eventWithin(userAreaElement, event)) {
|
||||
setChannelListExpandedByInteraction(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPanelCollapsed("channelList") && eventWithin(channelListElement, event)) {
|
||||
setChannelListExpandedByInteraction(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSidebarLeave(event: ReactFocusEvent<HTMLElement> | ReactMouseEvent<HTMLElement>) {
|
||||
if (nodeWithin(userAreaElement, event.relatedTarget)) {
|
||||
setChannelListExpandedByInteraction(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventWithin(channelListElement, event) && !nodeWithin(channelListElement, event.relatedTarget)) {
|
||||
setChannelListExpandedByInteraction(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (containsRelatedTarget(event)) return;
|
||||
if (isPanelCollapsed("channelList")) setChannelListExpandedByInteraction(false);
|
||||
}
|
||||
|
||||
function isInteractiveDragTarget(node: unknown) {
|
||||
if (!(node instanceof HTMLElement)) return false;
|
||||
|
||||
if (node.isContentEditable) return true;
|
||||
|
||||
switch (node.tagName) {
|
||||
case "A":
|
||||
case "BUTTON":
|
||||
case "INPUT":
|
||||
case "SELECT":
|
||||
case "TEXTAREA":
|
||||
return true;
|
||||
}
|
||||
|
||||
return node.getAttribute("role") === "button" || node.getAttribute("aria-haspopup") === "menu";
|
||||
}
|
||||
|
||||
function shouldDetachUserArea() {
|
||||
return settings.plain.detachUserArea && !isPanelCollapsed("userArea");
|
||||
}
|
||||
|
||||
function setChannelListElement(element: HTMLElement | null) {
|
||||
channelListElement = element;
|
||||
}
|
||||
|
||||
function setUserAreaElement(element: HTMLElement | null) {
|
||||
userAreaElement = element;
|
||||
}
|
||||
|
||||
function syncUserAreaDetachState() {
|
||||
notifySurfaceClassesChanged("sidebar");
|
||||
notifySurfaceClassesChanged("userArea");
|
||||
}
|
||||
|
||||
function clampUserAreaPosition(x: number, y: number, width = DETACHED_USER_AREA_WIDTH, height = DETACHED_USER_AREA_HEIGHT) {
|
||||
const maxX = Math.max(DETACHED_USER_AREA_MARGIN, window.innerWidth - width - DETACHED_USER_AREA_MARGIN);
|
||||
const maxY = Math.max(DETACHED_USER_AREA_MARGIN, window.innerHeight - height - DETACHED_USER_AREA_MARGIN);
|
||||
|
||||
return {
|
||||
x: Math.min(Math.max(DETACHED_USER_AREA_MARGIN, x), maxX),
|
||||
y: Math.min(Math.max(DETACHED_USER_AREA_MARGIN, y), maxY),
|
||||
};
|
||||
}
|
||||
|
||||
function getDetachedUserAreaPosition() {
|
||||
if (detachedUserAreaDragPosition) return detachedUserAreaDragPosition;
|
||||
|
||||
const storedX = settings.plain.detachedUserAreaX;
|
||||
const storedY = settings.plain.detachedUserAreaY;
|
||||
const defaultPosition = clampUserAreaPosition(
|
||||
window.innerWidth - DETACHED_USER_AREA_WIDTH - DETACHED_USER_AREA_DEFAULT_OFFSET_X,
|
||||
window.innerHeight - DETACHED_USER_AREA_HEIGHT - DETACHED_USER_AREA_DEFAULT_OFFSET_Y
|
||||
);
|
||||
|
||||
if (!Number.isFinite(storedX) || !Number.isFinite(storedY) || storedX < 0 || storedY < 0) return defaultPosition;
|
||||
return clampUserAreaPosition(storedX, storedY);
|
||||
}
|
||||
|
||||
function persistDetachedUserAreaPosition(x: number, y: number, width?: number, height?: number) {
|
||||
const position = clampUserAreaPosition(x, y, width, height);
|
||||
const roundedX = Math.round(position.x);
|
||||
const roundedY = Math.round(position.y);
|
||||
if (settings.plain.detachedUserAreaX === roundedX && settings.plain.detachedUserAreaY === roundedY) return;
|
||||
|
||||
settings.store.detachedUserAreaX = roundedX;
|
||||
settings.store.detachedUserAreaY = roundedY;
|
||||
}
|
||||
|
||||
function scheduleDetachedUserAreaUpdate() {
|
||||
if (detachedUserAreaAnimationFrame != null) return;
|
||||
|
||||
detachedUserAreaAnimationFrame = requestAnimationFrame(() => {
|
||||
detachedUserAreaAnimationFrame = undefined;
|
||||
notifySurfaceClassesChanged("userArea");
|
||||
});
|
||||
}
|
||||
|
||||
function cancelDetachedUserAreaUpdate() {
|
||||
if (detachedUserAreaAnimationFrame == null) return;
|
||||
|
||||
cancelAnimationFrame(detachedUserAreaAnimationFrame);
|
||||
detachedUserAreaAnimationFrame = undefined;
|
||||
}
|
||||
|
||||
function handleDetachedUserAreaMouseMove(event: MouseEvent) {
|
||||
if (!userAreaDragState) return;
|
||||
const position = clampUserAreaPosition(
|
||||
event.clientX - userAreaDragState.offsetX,
|
||||
event.clientY - userAreaDragState.offsetY,
|
||||
userAreaDragState.width,
|
||||
userAreaDragState.height
|
||||
);
|
||||
if (detachedUserAreaDragPosition?.x === position.x && detachedUserAreaDragPosition.y === position.y) return;
|
||||
|
||||
detachedUserAreaDragPosition = position;
|
||||
detachedUserAreaPositionChanged = true;
|
||||
scheduleDetachedUserAreaUpdate();
|
||||
}
|
||||
|
||||
function stopDetachedUserAreaDrag() {
|
||||
const dragState = userAreaDragState;
|
||||
cancelDetachedUserAreaUpdate();
|
||||
if (dragState && detachedUserAreaDragPosition && detachedUserAreaPositionChanged) {
|
||||
persistDetachedUserAreaPosition(detachedUserAreaDragPosition.x, detachedUserAreaDragPosition.y, dragState.width, dragState.height);
|
||||
}
|
||||
|
||||
userAreaDragState = undefined;
|
||||
detachedUserAreaDragPosition = undefined;
|
||||
detachedUserAreaPositionChanged = false;
|
||||
document.removeEventListener("mousemove", handleDetachedUserAreaMouseMove, true);
|
||||
document.removeEventListener("mouseup", stopDetachedUserAreaDrag, true);
|
||||
}
|
||||
|
||||
function startDetachedUserAreaDrag(event: ReactMouseEvent<HTMLElement>) {
|
||||
if (!shouldDetachUserArea() || event.button !== 0) return;
|
||||
|
||||
const path = event.nativeEvent.composedPath();
|
||||
if (path.some(node => node !== event.currentTarget && isInteractiveDragTarget(node))) return;
|
||||
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const height = Math.min(rect.height, DETACHED_USER_AREA_HEIGHT);
|
||||
detachedUserAreaDragPosition = getDetachedUserAreaPosition();
|
||||
detachedUserAreaPositionChanged = false;
|
||||
userAreaDragState = {
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top,
|
||||
width: rect.width,
|
||||
height,
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleDetachedUserAreaMouseMove, true);
|
||||
document.addEventListener("mouseup", stopDetachedUserAreaDrag, true);
|
||||
}
|
||||
|
||||
const ToolbarMenu = ErrorBoundary.wrap(({ onClose }: { onClose(): void; }) => {
|
||||
const store = settings.use(collapseSettingKeys);
|
||||
|
||||
return (
|
||||
<Menu.Menu navId="vc-collapsible-ui-toolbar-menu" onClose={onClose} aria-label="Collapsible UI">
|
||||
{toolbarPanelOrder.map(panelId => {
|
||||
const panel = panelRegistry[panelId];
|
||||
const collapsed = store[panel.collapsedKey];
|
||||
|
||||
return (
|
||||
<Menu.MenuCheckboxItem
|
||||
key={panelId}
|
||||
id={`vc-collapsible-ui-${panel.classId}`}
|
||||
label={panel.label}
|
||||
checked={!collapsed}
|
||||
action={() => togglePanel(panelId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu.Menu>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
const ToolbarButtons = ErrorBoundary.wrap(() => {
|
||||
const store = settings.use(collapseSettingKeys);
|
||||
const anyCollapsed = toolbarPanelOrder.some(panelId => store[panelRegistry[panelId].collapsedKey]);
|
||||
|
||||
return (
|
||||
<ChannelToolbarButton
|
||||
icon={PanelsIcon}
|
||||
tooltip="Collapsible UI"
|
||||
aria-label="Collapsible UI"
|
||||
selected={anyCollapsed}
|
||||
onClick={openToolbarMenu}
|
||||
onContextMenu={openToolbarMenu}
|
||||
/>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
const CollapsedMenuButton = ErrorBoundary.wrap(() => (
|
||||
<Clickable
|
||||
className={cl("restore-button")}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Collapsible UI"
|
||||
onClick={openToolbarMenu}
|
||||
onContextMenu={openToolbarMenu}
|
||||
>
|
||||
<PanelsIcon width={18} height={18} />
|
||||
</Clickable>
|
||||
), { noop: true });
|
||||
|
||||
const ChatButtonsRow = ErrorBoundary.wrap(({ buttons }: { buttons: ReactNode[]; }) => {
|
||||
const chatButtonsCollapsed = usePanelCollapsed("chatButtons");
|
||||
|
||||
if (buttons.length === 0) return <>{buttons}</>;
|
||||
|
||||
return (
|
||||
<div className={classes(cl("chat-buttons"), chatButtonsCollapsed && cl("chat-buttons-collapsed"))}>
|
||||
<div className={cl("chat-buttons-items")}>
|
||||
{buttons}
|
||||
</div>
|
||||
<CollapsedMenuButton />
|
||||
</div>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
export default definePlugin({
|
||||
name: "CollapsibleUI",
|
||||
description: "Native collapsible channel, member, chat button, and user area surfaces.",
|
||||
tags: ["Appearance", "Customisation", "Chat", "Servers"],
|
||||
dependencies: ["HeaderBarAPI", "ChatInputButtonAPI", "SurfaceClassesAPI"],
|
||||
authors: [EquicordDevs.benjii],
|
||||
searchTerms: ["ui", "sidebar", "collapsible"],
|
||||
managedStyle,
|
||||
settings,
|
||||
|
||||
headerBarButton: {
|
||||
icon: PanelsIcon,
|
||||
location: "channeltoolbar",
|
||||
priority: 25,
|
||||
render: () => <ToolbarButtons />,
|
||||
},
|
||||
|
||||
chatBarButtonWrapper: {
|
||||
wrapper: (buttons: ReactNode) => {
|
||||
if (!Array.isArray(buttons) || buttons.length === 0) return buttons;
|
||||
return <ChatButtonsRow buttons={buttons} />;
|
||||
},
|
||||
priority: 0,
|
||||
},
|
||||
|
||||
start() {
|
||||
const panelAttr = (classId: string, collapsed: boolean): SurfaceProvidedProps => ({
|
||||
[`data-vc-collapsible-ui-${classId}`]: "",
|
||||
[`data-vc-collapsible-ui-${classId}-${collapsed ? "collapsed" : "expanded"}`]: "",
|
||||
} as SurfaceProvidedProps);
|
||||
|
||||
providerUnsubs = [
|
||||
addSurfacePropsProvider("guildBar", () => panelAttr(panelRegistry.guildBar.classId, isPanelCollapsed("guildBar"))),
|
||||
addSurfacePropsProvider("channelList", () => {
|
||||
const attrs: SurfaceProvidedProps = panelAttr(panelRegistry.channelList.classId, isPanelCollapsed("channelList"));
|
||||
attrs.ref = setChannelListElement;
|
||||
return attrs;
|
||||
}),
|
||||
addSurfacePropsProvider("membersList", () => panelAttr(panelRegistry.membersList.classId, isPanelCollapsed("membersList"))),
|
||||
addSurfacePropsProvider("titleBar", () => panelAttr(panelRegistry.titleBar.classId, isPanelCollapsed("titleBar"))),
|
||||
addSurfacePropsProvider("headerBar", () => {
|
||||
const collapsed = isPanelCollapsed("headerBar");
|
||||
const attrs: SurfaceProvidedProps = panelAttr(panelRegistry.headerBar.classId, collapsed);
|
||||
if (collapsed && headerBarExpandedByInteraction) {
|
||||
attrs["data-vc-collapsible-ui-header-bar-interaction-expanded"] = "";
|
||||
}
|
||||
attrs.onFocusCapture = () => {
|
||||
if (isPanelCollapsed("headerBar")) setHeaderBarExpandedByInteraction(true);
|
||||
};
|
||||
attrs.onBlurCapture = event => {
|
||||
if (containsRelatedTarget(event)) return;
|
||||
if (isPanelCollapsed("headerBar")) setHeaderBarExpandedByInteraction(false);
|
||||
};
|
||||
return attrs;
|
||||
}),
|
||||
addSurfacePropsProvider("userArea", () => {
|
||||
const uaCollapsed = isPanelCollapsed("userArea");
|
||||
const clCollapsed = isPanelCollapsed("channelList");
|
||||
const gbCollapsed = isPanelCollapsed("guildBar");
|
||||
const userAreaDetached = shouldDetachUserArea();
|
||||
const attrs: SurfaceProvidedProps = panelAttr(panelRegistry.userArea.classId, uaCollapsed);
|
||||
attrs.ref = setUserAreaElement;
|
||||
if (userAreaDetached) {
|
||||
const position = getDetachedUserAreaPosition();
|
||||
attrs["data-vc-collapsible-ui-user-area-detached"] = "";
|
||||
attrs.style = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
|
||||
};
|
||||
attrs.onMouseDownCapture = startDetachedUserAreaDrag;
|
||||
}
|
||||
if (!userAreaDetached && clCollapsed) {
|
||||
attrs["data-vc-collapsible-ui-user-area-channel-list-collapsed"] = "";
|
||||
}
|
||||
if (!userAreaDetached && gbCollapsed) {
|
||||
attrs["data-vc-collapsible-ui-user-area-guild-bar-collapsed"] = "";
|
||||
}
|
||||
return attrs;
|
||||
}),
|
||||
addSurfacePropsProvider("base", () => {
|
||||
const channelListCollapsed = isPanelCollapsed("channelList");
|
||||
const headerBarCollapsed = isPanelCollapsed("headerBar");
|
||||
return {
|
||||
"data-vc-collapsible-ui-base": "",
|
||||
[`data-vc-collapsible-ui-base-channel-list-${channelListCollapsed ? "collapsed" : "expanded"}`]: "",
|
||||
...(channelListCollapsed && channelListExpandedByInteraction ? { "data-vc-collapsible-ui-base-channel-list-interaction-expanded": "" } : {}),
|
||||
...(headerBarCollapsed && !headerBarExpandedByInteraction ? { "data-vc-collapsible-ui-base-header-bar-collapsed": "" } : {}),
|
||||
...(headerBarCollapsed && headerBarExpandedByInteraction ? { "data-vc-collapsible-ui-base-header-bar-expanded": "" } : {}),
|
||||
} as SurfaceProvidedProps;
|
||||
}),
|
||||
addSurfacePropsProvider("sidebar", () => {
|
||||
const collapsed = isPanelCollapsed("channelList");
|
||||
const userAreaDetached = shouldDetachUserArea();
|
||||
return {
|
||||
"data-vc-collapsible-ui-sidebar": "",
|
||||
[`data-vc-collapsible-ui-sidebar-channel-list-${collapsed ? "collapsed" : "expanded"}`]: "",
|
||||
...(collapsed && channelListExpandedByInteraction ? { "data-vc-collapsible-ui-sidebar-channel-list-expanded": "" } : {}),
|
||||
...(userAreaDetached ? { "data-vc-collapsible-ui-sidebar-user-area-detached": "" } : {}),
|
||||
onFocusCapture: handleSidebarEnter,
|
||||
onBlurCapture: handleSidebarLeave,
|
||||
onMouseOverCapture: handleSidebarEnter,
|
||||
onMouseOutCapture: handleSidebarLeave,
|
||||
} as SurfaceProvidedProps;
|
||||
}),
|
||||
];
|
||||
|
||||
setCollapseSettingChangeHandler(syncPanelCollapsedState);
|
||||
setUserAreaDetachSettingChangeHandler(syncUserAreaDetachState);
|
||||
syncAllPanelCollapsedStates();
|
||||
},
|
||||
|
||||
stop() {
|
||||
setCollapseSettingChangeHandler(undefined);
|
||||
setUserAreaDetachSettingChangeHandler(undefined);
|
||||
stopDetachedUserAreaDrag();
|
||||
providerUnsubs.forEach(unsub => unsub());
|
||||
providerUnsubs = [];
|
||||
setHeaderBarPointerTrackerEnabled(false);
|
||||
channelListElement = null;
|
||||
userAreaElement = null;
|
||||
channelListExpandedByInteraction = false;
|
||||
headerBarExpandedByInteraction = false;
|
||||
surfaceCssPxCache.clear();
|
||||
},
|
||||
});
|
||||
149
src/equicordplugins/collapsibleUi/settings.ts
Normal file
149
src/equicordplugins/collapsibleUi/settings.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
|
||||
export const panelRegistry = {
|
||||
guildBar: {
|
||||
id: "guildBar",
|
||||
classId: "guild-bar",
|
||||
label: "Guild Bar",
|
||||
collapsedKey: "guildBarCollapsed",
|
||||
},
|
||||
channelList: {
|
||||
id: "channelList",
|
||||
classId: "channel-list",
|
||||
label: "Channel List",
|
||||
collapsedKey: "channelListCollapsed",
|
||||
},
|
||||
membersList: {
|
||||
id: "membersList",
|
||||
classId: "members-list",
|
||||
label: "Members List",
|
||||
collapsedKey: "membersListCollapsed",
|
||||
},
|
||||
chatButtons: {
|
||||
id: "chatButtons",
|
||||
classId: "chat-buttons",
|
||||
label: "Message Buttons",
|
||||
collapsedKey: "chatButtonsCollapsed",
|
||||
},
|
||||
titleBar: {
|
||||
id: "titleBar",
|
||||
classId: "title-bar",
|
||||
label: "Title Bar",
|
||||
collapsedKey: "titleBarCollapsed",
|
||||
},
|
||||
headerBar: {
|
||||
id: "headerBar",
|
||||
classId: "header-bar",
|
||||
label: "Header Bar",
|
||||
collapsedKey: "headerBarCollapsed",
|
||||
},
|
||||
userArea: {
|
||||
id: "userArea",
|
||||
classId: "user-area",
|
||||
label: "User Area",
|
||||
collapsedKey: "userAreaCollapsed",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PanelId = keyof typeof panelRegistry;
|
||||
|
||||
export const toolbarPanelOrder = ["guildBar", "channelList", "membersList", "chatButtons", "titleBar", "headerBar", "userArea"] as const satisfies readonly PanelId[];
|
||||
|
||||
export const collapseSettingKeys = toolbarPanelOrder.map(panelId => panelRegistry[panelId].collapsedKey);
|
||||
|
||||
type CollapseSettingChangeHandler = (panelId: PanelId, collapsed: boolean) => void;
|
||||
type UserAreaDetachSettingChangeHandler = () => void;
|
||||
|
||||
let collapseSettingChangeHandler: CollapseSettingChangeHandler | undefined;
|
||||
let userAreaDetachSettingChangeHandler: UserAreaDetachSettingChangeHandler | undefined;
|
||||
|
||||
export function setCollapseSettingChangeHandler(handler: CollapseSettingChangeHandler | undefined) {
|
||||
collapseSettingChangeHandler = handler;
|
||||
}
|
||||
|
||||
export function setUserAreaDetachSettingChangeHandler(handler: UserAreaDetachSettingChangeHandler | undefined) {
|
||||
userAreaDetachSettingChangeHandler = handler;
|
||||
}
|
||||
|
||||
function onCollapseSettingChanged(panelId: PanelId) {
|
||||
return (collapsed: boolean) => collapseSettingChangeHandler?.(panelId, collapsed);
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
detachUserArea: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Detach the user area so it can be moved freely when it is not collapsed.",
|
||||
default: false,
|
||||
onChange: () => userAreaDetachSettingChangeHandler?.(),
|
||||
},
|
||||
detachedUserAreaX: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Persist the detached user area x position.",
|
||||
default: -1,
|
||||
hidden: true,
|
||||
onChange: () => userAreaDetachSettingChangeHandler?.(),
|
||||
},
|
||||
detachedUserAreaY: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Persist the detached user area y position.",
|
||||
default: -1,
|
||||
hidden: true,
|
||||
onChange: () => userAreaDetachSettingChangeHandler?.(),
|
||||
},
|
||||
guildBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the guild bar as collapsed.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: onCollapseSettingChanged("guildBar"),
|
||||
},
|
||||
channelListCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the channel list as collapsed.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: onCollapseSettingChanged("channelList"),
|
||||
},
|
||||
membersListCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the members list as collapsed.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: onCollapseSettingChanged("membersList"),
|
||||
},
|
||||
chatButtonsCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the message button row as collapsed.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: onCollapseSettingChanged("chatButtons"),
|
||||
},
|
||||
titleBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the title bar as collapsed.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: onCollapseSettingChanged("titleBar"),
|
||||
},
|
||||
headerBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the header bar as collapsed.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: onCollapseSettingChanged("headerBar"),
|
||||
},
|
||||
userAreaCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the user area as collapsed.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: onCollapseSettingChanged("userArea"),
|
||||
},
|
||||
});
|
||||
349
src/equicordplugins/collapsibleUi/style.css
Normal file
349
src/equicordplugins/collapsibleUi/style.css
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
[data-vc-collapsible-ui-base],
|
||||
[data-vc-collapsible-ui-guild-bar],
|
||||
[data-vc-collapsible-ui-channel-list],
|
||||
[data-vc-collapsible-ui-members-list],
|
||||
[data-vc-collapsible-ui-title-bar],
|
||||
[data-vc-collapsible-ui-header-bar],
|
||||
[data-vc-collapsible-ui-user-area],
|
||||
.vc-collapsible-ui-chat-buttons {
|
||||
--vc-cui-peek-size: 12px;
|
||||
--vc-cui-user-area-collapsed-width: 52px;
|
||||
--vc-cui-header-bar-height: 32px;
|
||||
--vc-cui-collapsed-block-size: 8px;
|
||||
--vc-cui-guild-bar-expanded-width: 64px;
|
||||
--vc-cui-channel-list-min-width: 264px;
|
||||
--vc-cui-channel-list-max-width: 432px;
|
||||
--vc-cui-members-list-expanded-width: 240px;
|
||||
--vc-cui-title-bar-expanded-height: 46px;
|
||||
--vc-cui-chat-buttons-offset: 24px;
|
||||
--vc-cui-restore-button-size: 24px;
|
||||
--vc-cui-fade-delay: 80ms;
|
||||
--vc-cui-transition: 140ms ease;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-base-channel-list-collapsed] {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] var(--vc-cui-peek-size) [channelsEnd] 1fr [end] !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-base-channel-list-collapsed][data-vc-collapsible-ui-base-channel-list-interaction-expanded] {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] var(--custom-guild-sidebar-width, 312px) [channelsEnd] 1fr [end] !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-guild-bar],
|
||||
[data-vc-collapsible-ui-channel-list],
|
||||
[data-vc-collapsible-ui-members-list],
|
||||
[data-vc-collapsible-ui-title-bar],
|
||||
[data-vc-collapsible-ui-header-bar],
|
||||
[data-vc-collapsible-ui-user-area] {
|
||||
overflow: hidden;
|
||||
transition: opacity var(--vc-cui-transition);
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-user-area-detached] {
|
||||
--custom-app-panels-height: 0px !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-collapsed] {
|
||||
z-index: 5;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-guild-bar-collapsed],
|
||||
[data-vc-collapsible-ui-channel-list-collapsed],
|
||||
[data-vc-collapsible-ui-members-list-collapsed] {
|
||||
width: var(--vc-cui-peek-size) !important;
|
||||
min-width: var(--vc-cui-peek-size) !important;
|
||||
max-width: var(--vc-cui-peek-size) !important;
|
||||
flex: 0 0 var(--vc-cui-peek-size) !important;
|
||||
opacity: 0.01;
|
||||
|
||||
/* Delay only the opacity fade; dimensions change immediately to avoid animated layout work. */
|
||||
transition-delay: var(--vc-cui-fade-delay);
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-guild-bar-collapsed]:is(:hover, :focus-within) {
|
||||
width: var(--vc-cui-guild-bar-expanded-width) !important;
|
||||
min-width: var(--vc-cui-guild-bar-expanded-width) !important;
|
||||
max-width: var(--vc-cui-guild-bar-expanded-width) !important;
|
||||
flex-basis: var(--vc-cui-guild-bar-expanded-width) !important;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-channel-list-expanded],
|
||||
[data-vc-collapsible-ui-channel-list-collapsed]:is(:hover, :focus-within) {
|
||||
width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
min-width: var(--vc-cui-channel-list-min-width) !important;
|
||||
max-width: var(--vc-cui-channel-list-max-width) !important;
|
||||
flex-basis: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-expanded] {
|
||||
width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
min-width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
max-width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-expanded] [data-vc-collapsible-ui-channel-list-collapsed] {
|
||||
width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
min-width: var(--vc-cui-channel-list-min-width) !important;
|
||||
max-width: var(--vc-cui-channel-list-max-width) !important;
|
||||
flex-basis: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-members-list-collapsed] {
|
||||
flex: 0 1 auto !important;
|
||||
flex-basis: auto !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-members-list-collapsed]:is(:hover, :focus-within) {
|
||||
width: var(--vc-cui-members-list-expanded-width) !important;
|
||||
min-width: var(--vc-cui-members-list-expanded-width) !important;
|
||||
max-width: var(--vc-cui-members-list-expanded-width) !important;
|
||||
flex-basis: auto !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-title-bar-collapsed] {
|
||||
height: var(--vc-cui-collapsed-block-size) !important;
|
||||
min-height: var(--vc-cui-collapsed-block-size) !important;
|
||||
max-height: var(--vc-cui-collapsed-block-size) !important;
|
||||
padding-block: 0 !important;
|
||||
color: transparent !important;
|
||||
opacity: 0.01 !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-title-bar-collapsed]:is(:hover, :focus-within) {
|
||||
height: var(--vc-cui-title-bar-expanded-height) !important;
|
||||
min-height: var(--vc-cui-title-bar-expanded-height) !important;
|
||||
max-height: var(--vc-cui-title-bar-expanded-height) !important;
|
||||
color: inherit !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-base-header-bar-collapsed] {
|
||||
--custom-app-top-bar-height: var(--vc-cui-collapsed-block-size) !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-base-header-bar-expanded] {
|
||||
--custom-app-top-bar-height: var(--vc-cui-header-bar-height) !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-header-bar-collapsed] {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
height: var(--vc-cui-collapsed-block-size) !important;
|
||||
min-height: var(--vc-cui-collapsed-block-size) !important;
|
||||
max-height: var(--vc-cui-collapsed-block-size) !important;
|
||||
opacity: 0.01;
|
||||
-webkit-app-region: drag;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-header-bar-expanded] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-header-bar-collapsed]::before {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset: 0;
|
||||
height: var(--vc-cui-collapsed-block-size);
|
||||
content: "";
|
||||
pointer-events: auto;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-header-bar-collapsed] > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-user-area-collapsed] > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-header-bar-collapsed][data-vc-collapsible-ui-header-bar-interaction-expanded] {
|
||||
height: var(--vc-cui-header-bar-height) !important;
|
||||
min-height: var(--vc-cui-header-bar-height) !important;
|
||||
max-height: var(--vc-cui-header-bar-height) !important;
|
||||
opacity: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-header-bar-collapsed][data-vc-collapsible-ui-header-bar-interaction-expanded]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-header-bar-collapsed][data-vc-collapsible-ui-header-bar-interaction-expanded] > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-user-area] {
|
||||
background: var(--background-secondary-alt) !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-user-area-collapsed] {
|
||||
height: var(--vc-cui-peek-size) !important;
|
||||
min-height: var(--vc-cui-peek-size) !important;
|
||||
max-height: var(--vc-cui-peek-size) !important;
|
||||
padding-block: 0 !important;
|
||||
border-top-width: 0 !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-user-area-collapsed]:is(:hover, :focus-within) {
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-user-area-collapsed]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-collapsed]:not([data-vc-collapsible-ui-sidebar-user-area-detached]) [data-vc-collapsible-ui-user-area] {
|
||||
width: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
min-width: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
max-width: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-collapsed]:not([data-vc-collapsible-ui-sidebar-user-area-detached]) [data-vc-collapsible-ui-user-area-expanded] {
|
||||
position: absolute !important;
|
||||
right: auto !important;
|
||||
bottom: 6px !important;
|
||||
left: 6px !important;
|
||||
height: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
min-height: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
max-height: var(--vc-cui-user-area-collapsed-width) !important;
|
||||
flex: 0 0 auto !important;
|
||||
align-self: flex-start;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-collapsed]:not([data-vc-collapsible-ui-sidebar-user-area-detached]) [data-vc-collapsible-ui-user-area-expanded] > * {
|
||||
width: var(--custom-guild-sidebar-width, 312px);
|
||||
min-width: var(--vc-cui-channel-list-min-width);
|
||||
max-width: var(--vc-cui-channel-list-max-width);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-collapsed]:not([data-vc-collapsible-ui-sidebar-user-area-detached]) [data-vc-collapsible-ui-user-area-expanded]:is(:hover, :focus-within) {
|
||||
width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
min-width: var(--vc-cui-channel-list-min-width) !important;
|
||||
max-width: var(--vc-cui-channel-list-max-width) !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-collapsed]:not([data-vc-collapsible-ui-sidebar-user-area-detached]) [data-vc-collapsible-ui-user-area-guild-bar-collapsed][data-vc-collapsible-ui-user-area-collapsed] {
|
||||
width: var(--vc-cui-peek-size) !important;
|
||||
min-width: var(--vc-cui-peek-size) !important;
|
||||
max-width: var(--vc-cui-peek-size) !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-sidebar-channel-list-expanded]:not([data-vc-collapsible-ui-sidebar-user-area-detached]) [data-vc-collapsible-ui-user-area] {
|
||||
width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
min-width: var(--vc-cui-channel-list-min-width) !important;
|
||||
max-width: var(--vc-cui-channel-list-max-width) !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-user-area-detached] {
|
||||
position: fixed !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
min-width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
max-width: var(--custom-guild-sidebar-width, 312px) !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
cursor: grab;
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
[data-vc-collapsible-ui-user-area-detached]:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-inline-size: 0;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform var(--vc-cui-transition),
|
||||
opacity var(--vc-cui-transition);
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons-collapsed .vc-collapsible-ui-chat-buttons-items {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: var(--vc-cui-chat-buttons-offset);
|
||||
width: max-content;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%) scaleX(0);
|
||||
transition-delay: var(--vc-cui-fade-delay);
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons-collapsed:is(:hover, :focus-within) .vc-collapsible-ui-chat-buttons-items {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(-50%) scaleX(1);
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-restore-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--vc-cui-restore-button-size);
|
||||
height: var(--vc-cui-restore-button-size);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--interactive-normal);
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-restore-button:hover {
|
||||
color: var(--interactive-hover);
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.vc-collapsible-ui-chat-buttons:not(.vc-collapsible-ui-chat-buttons-collapsed) .vc-collapsible-ui-restore-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-vc-collapsible-ui-guild-bar],
|
||||
[data-vc-collapsible-ui-channel-list],
|
||||
[data-vc-collapsible-ui-members-list],
|
||||
[data-vc-collapsible-ui-title-bar],
|
||||
[data-vc-collapsible-ui-header-bar],
|
||||
[data-vc-collapsible-ui-user-area],
|
||||
.vc-collapsible-ui-chat-buttons,
|
||||
.vc-collapsible-ui-chat-buttons-items {
|
||||
--vc-cui-transition: 0ms;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue