From cc01bb70cb8485113b7f6bdbcff249005384ce20 Mon Sep 17 00:00:00 2001 From: V Date: Sat, 11 Oct 2025 01:42:14 +0200 Subject: [PATCH] Replace Forms with our own independent components (#3618) Vastly improves resilience of Settings Ui against Discord changes. Should now never break even if hell breaks loose Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- browser/content.js | 2 +- packages/discord-types/src/components.d.ts | 83 ++------ scripts/build/build.mjs | 3 +- scripts/build/buildWeb.mjs | 5 +- scripts/build/common.mjs | 1 + src/Vencord.ts | 10 +- src/api/ChatButtons.tsx | 21 +- src/components/BaseText.css | 8 + src/components/BaseText.tsx | 100 +++++++++ src/components/Button.css | 199 ++++++++++++++++++ src/components/Button.tsx | 107 ++++++++++ src/components/Divider.css | 6 + src/components/Divider.tsx | 21 ++ src/components/FormDivider.css | 5 - src/components/FormDivider.tsx | 13 -- src/components/FormSwitch.tsx | 19 +- src/components/Heading.css | 65 ++++++ src/components/Heading.tsx | 60 ++++++ src/components/Icons.tsx | 7 +- src/components/Paragraph.tsx | 17 ++ src/components/Span.tsx | 17 ++ src/components/Switch.css | 49 +++++ src/components/Switch.tsx | 24 +-- src/components/css.ts | 15 ++ src/components/iconStyles.css | 11 +- src/components/index.ts | 7 +- src/components/margins.ts | 43 ++++ src/components/settings/DonateButton.tsx | 2 +- src/components/settings/SpecialCard.css | 3 +- src/components/settings/SpecialCard.tsx | 3 +- src/components/settings/tabs/BaseTab.tsx | 11 +- .../tabs/patchHelper/ReplacementInput.tsx | 4 +- .../settings/tabs/patchHelper/index.tsx | 14 +- .../settings/tabs/plugins/PluginCard.tsx | 7 +- .../settings/tabs/plugins/PluginModal.tsx | 58 ++--- .../settings/tabs/plugins/index.tsx | 48 ++--- .../settings/tabs/plugins/styles.css | 8 +- .../settings/tabs/sync/CloudTab.tsx | 12 +- .../settings/tabs/themes/CspErrorCard.tsx | 5 +- .../settings/tabs/themes/OnlineThemesTab.tsx | 2 +- .../settings/tabs/updater/index.tsx | 3 +- .../settings/tabs/vencord/DonateButton.tsx | 2 +- .../tabs/vencord/NotificationSettings.tsx | 6 +- .../settings/tabs/vencord/index.tsx | 72 ++++--- src/globals.d.ts | 1 + src/plugins/_api/badges/index.tsx | 22 +- src/plugins/_core/supportHelper.tsx | 2 + .../components/RenameButton.tsx | 13 +- .../betterSessions/components/RenameModal.tsx | 27 +-- src/plugins/betterSessions/index.tsx | 2 + src/plugins/betterSessions/styles.css | 10 + src/plugins/customRPC/index.tsx | 3 +- .../decor/ui/components/DecorSection.tsx | 13 +- .../decor/ui/modals/ChangeDecorationModal.tsx | 62 +++--- .../decor/ui/modals/CreateDecorationModal.tsx | 36 ++-- .../decor/ui/modals/GuidelinesModal.tsx | 35 +-- src/plugins/decor/ui/styles.css | 17 +- src/plugins/experiments/index.tsx | 16 +- src/plugins/fakeProfileThemes/index.tsx | 5 +- src/plugins/implicitRelationships/index.ts | 17 +- src/plugins/invisibleChat.desktop/index.tsx | 1 - src/plugins/messageLogger/index.tsx | 26 ++- .../components/RolesAndUsersPermissions.tsx | 2 +- .../components/UserPermissions.tsx | 3 +- src/plugins/permissionsViewer/index.tsx | 3 +- .../pinDms/components/CreateCategoryModal.tsx | 3 +- .../readAllNotificationsButton/index.tsx | 11 +- .../readAllNotificationsButton/style.css | 4 +- src/plugins/reviewDB/index.tsx | 14 +- .../spotifyControls/PlayerComponent.tsx | 32 +-- src/plugins/textReplace/index.tsx | 34 ++- src/plugins/themeAttributes/index.ts | 20 +- src/plugins/translate/TranslateModal.tsx | 3 +- src/plugins/viewRaw/index.tsx | 3 +- src/plugins/whoReacted/index.tsx | 2 +- src/utils/css.ts | 12 ++ src/utils/discord.tsx | 6 +- src/utils/margins.ts | 19 +- src/utils/quickCss.ts | 15 +- src/webpack/common/FormText.tsx | 25 --- src/webpack/common/components.ts | 38 ++-- src/webpack/common/internal.tsx | 10 +- src/webpack/webpack.ts | 12 ++ 83 files changed, 1233 insertions(+), 524 deletions(-) create mode 100644 src/components/BaseText.css create mode 100644 src/components/BaseText.tsx create mode 100644 src/components/Button.css create mode 100644 src/components/Button.tsx create mode 100644 src/components/Divider.css create mode 100644 src/components/Divider.tsx delete mode 100644 src/components/FormDivider.css delete mode 100644 src/components/FormDivider.tsx create mode 100644 src/components/Heading.css create mode 100644 src/components/Heading.tsx create mode 100644 src/components/Paragraph.tsx create mode 100644 src/components/Span.tsx create mode 100644 src/components/css.ts create mode 100644 src/components/margins.ts create mode 100644 src/plugins/betterSessions/styles.css create mode 100644 src/utils/css.ts delete mode 100644 src/webpack/common/FormText.tsx diff --git a/browser/content.js b/browser/content.js index 57964af64..2b51ebabf 100644 --- a/browser/content.js +++ b/browser/content.js @@ -10,7 +10,7 @@ style.href = browser.runtime.getURL("dist/Vencord.css"); document.addEventListener( "DOMContentLoaded", () => { - document.documentElement.append(style); + document.body.insertAdjacentElement("afterend", style); window.postMessage({ type: "vencord:meta", meta: { diff --git a/packages/discord-types/src/components.d.ts b/packages/discord-types/src/components.d.ts index 178cbdb00..76e03a918 100644 --- a/packages/discord-types/src/components.d.ts +++ b/packages/discord-types/src/components.d.ts @@ -1,49 +1,39 @@ import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref, RefObject } from "react"; -// copy(find(m => Array.isArray(m) && m.includes("heading-sm/normal")).map(JSON.stringify).join("|")) -export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg" | "code"; -export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>; -export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`; +// #region Old compability + +export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`; export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>; +// copy(find(m => Array.isArray(m) && m.includes("heading-sm/normal")).map(JSON.stringify).join("|")) +export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold"; + export type TextProps = PropsWithChildren & { variant?: TextVariant; tag?: "div" | "span" | "p" | "strong" | HeadingTag; - selectable?: boolean; - lineClamp?: number; }>; export type Text = ComponentType; -export type Heading = ComponentType; -export type FormTitle = ComponentType & PropsWithChildren<{ - /** default is h5 */ - tag?: HeadingTag; - faded?: boolean; - disabled?: boolean; - required?: boolean; - error?: ReactNode; -}>>; +export interface ButtonProps extends PropsWithChildren, "size">> { + /** Button.Looks.FILLED */ + look?: string; + /** Button.Colors.BRAND */ + color?: string; + /** Button.Sizes.MEDIUM */ + size?: string; -export type FormSection = ComponentType>; +} -export type FormDivider = ComponentType<{ - className?: string; - style?: CSSProperties; -}>; +export type Button = ComponentType & { + Colors: Record<"BRAND" | "RED" | "GREEN" | "PRIMARY" | "LINK" | "WHITE" | "TRANSPARENT" | "CUSTOM", string>; + Looks: Record<"FILLED" | "LINK", string>; + Sizes: Record<"NONE" | "SMALL" | "MEDIUM" | "LARGE" | "XLARGE" | "MIN", string>; +}; - -export type FormText = ComponentType; +// #endregion export type Tooltip = ComponentType<{ text: ReactNode | ComponentType; @@ -121,7 +111,6 @@ export type ComboboxPopout = ComponentType>; -export interface ButtonProps extends PropsWithChildren, "size">> { - /** Button.Looks.FILLED */ - look?: string; - /** Button.Colors.BRAND */ - color?: string; - /** Button.Sizes.MEDIUM */ - size?: string; - /** Button.BorderColors.BLACK */ - borderColor?: string; - - wrapperClassName?: string; - className?: string; - innerClassName?: string; - - buttonRef?: Ref; - focusProps?: any; - submitting?: boolean; - - submittingStartedLabel?: string; - submittingFinishedLabel?: string; -} - -export type Button = ComponentType & { - BorderColors: Record<"BLACK" | "BRAND" | "BRAND_NEW" | "GREEN" | "LINK" | "PRIMARY" | "RED" | "TRANSPARENT" | "WHITE" | "YELLOW", string>; - Colors: Record<"BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT" | "BRAND_NEW" | "CUSTOM", string>; - Hovers: Record<"DEFAULT" | "BRAND" | "RED" | "GREEN" | "YELLOW" | "PRIMARY" | "LINK" | "WHITE" | "BLACK" | "TRANSPARENT", string>; - Looks: Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>; - Sizes: Record<"NONE" | "TINY" | "SMALL" | "MEDIUM" | "LARGE" | "XLARGE" | "MIN" | "MAX" | "ICON", string>; - - Link: any; -}; - export type CheckboxAligns = { CENTER: "center"; TOP: "top"; diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index db0a1ce0f..6d0a43cce 100755 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -22,12 +22,13 @@ import { readdir } from "fs/promises"; import { join } from "path"; -import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_ANTI_CRASH_TEST, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from "./common.mjs"; const defines = stringifyValues({ IS_STANDALONE, IS_DEV, IS_REPORTER, + IS_ANTI_CRASH_TEST, IS_UPDATER_DISABLED, IS_WEB: false, IS_EXTENSION: false, diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index b22df8abe..e8269477a 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -24,7 +24,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises import { join } from "path"; import Zip from "zip-local"; -import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_ANTI_CRASH_TEST, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs"; /** * @type {import("esbuild").BuildOptions} @@ -47,6 +47,7 @@ const commonOptions = { IS_STANDALONE: true, IS_DEV, IS_REPORTER, + IS_ANTI_CRASH_TEST, IS_DISCORD_DESKTOP: false, IS_VESKTOP: false, IS_UPDATER_DISABLED: true, @@ -176,7 +177,7 @@ async function buildExtension(target, files) { const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => { const cssRuntime = ` -;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild( +;document.addEventListener("DOMContentLoaded", () => document.body.insertAdjacentElement("afterend", Object.assign(document.createElement("style"), { textContent: \`${content.replaceAll("`", "\\`")}\`, id: "vencord-css-core" diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 516f6a1b4..6e69ac088 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -42,6 +42,7 @@ export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now export const watch = process.argv.includes("--watch"); export const IS_DEV = watch || process.argv.includes("--dev"); export const IS_REPORTER = process.argv.includes("--reporter"); +export const IS_ANTI_CRASH_TEST = process.argv.includes("--anti-crash-test"); export const IS_STANDALONE = process.argv.includes("--standalone"); export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater"); diff --git a/src/Vencord.ts b/src/Vencord.ts index 00f293476..e5e8cc955 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -32,8 +32,10 @@ export { PlainSettings, Settings }; import "./utils/quickCss"; import "./webpack/patchWebpack"; +import { addVencordUiStyles } from "@components/css"; import { openUpdaterModal } from "@components/settings/tabs/updater"; import { IS_WINDOWS } from "@utils/constants"; +import { createAndAppendStyle } from "@utils/css"; import { StartAt } from "@utils/types"; import { get as dsGet } from "./api/DataStore"; @@ -163,12 +165,12 @@ startAllPlugins(StartAt.Init); init(); document.addEventListener("DOMContentLoaded", () => { + addVencordUiStyles(); + startAllPlugins(StartAt.DOMContentLoaded); + // FIXME if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && IS_WINDOWS) { - document.head.append(Object.assign(document.createElement("style"), { - id: "vencord-native-titlebar-style", - textContent: "[class*=titleBar]{display: none!important}" - })); + createAndAppendStyle("vencord-native-titlebar-style").textContent = "[class*=titleBar]{display: none!important}"; } }, { once: true }); diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx index 1cacd06b2..2fea24d3d 100644 --- a/src/api/ChatButtons.tsx +++ b/src/api/ChatButtons.tsx @@ -8,9 +8,10 @@ import "./ChatButton.css"; import ErrorBoundary from "@components/ErrorBoundary"; import { Logger } from "@utils/Logger"; +import { classes } from "@utils/misc"; import { Channel } from "@vencord/discord-types"; import { waitFor } from "@webpack"; -import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { ButtonWrapperClasses, Clickable, Tooltip } from "@webpack/common"; import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react"; let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>; @@ -97,32 +98,30 @@ export const removeChatBarButton = (id: string) => buttonFactories.delete(id); export interface ChatBarButtonProps { children: ReactNode; tooltip: string; - onClick: MouseEventHandler; - onContextMenu?: MouseEventHandler; - onAuxClick?: MouseEventHandler; - buttonProps?: Omit, "size" | "onClick" | "onContextMenu" | "onAuxClick">; + onClick: MouseEventHandler; + onContextMenu?: MouseEventHandler; + onAuxClick?: MouseEventHandler; + buttonProps?: Omit, "size" | "onClick" | "onContextMenu" | "onAuxClick">; } export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { return ( {({ onMouseEnter, onMouseLeave }) => (
- +
)}
diff --git a/src/components/BaseText.css b/src/components/BaseText.css new file mode 100644 index 000000000..d322a5679 --- /dev/null +++ b/src/components/BaseText.css @@ -0,0 +1,8 @@ +.vc-text-base { + font-family: var(--font-primary); + color: var(--text-default); + line-height: normal; + + /* Discord puts an insane default margin on p tags, so reset that here */ + margin: 0; +} \ No newline at end of file diff --git a/src/components/BaseText.tsx b/src/components/BaseText.tsx new file mode 100644 index 000000000..1abddf7a9 --- /dev/null +++ b/src/components/BaseText.tsx @@ -0,0 +1,100 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./BaseText.css"; + +import { classNameFactory } from "@api/Styles"; +import { classes } from "@utils/misc"; +import type { Text as DiscordText } from "@vencord/discord-types"; +import type { ComponentPropsWithoutRef, ReactNode } from "react"; + +const textCls = classNameFactory("vc-text-"); + +const Sizes = { + xxs: "0.625rem", + xs: "0.75rem", + sm: "0.875rem", + md: "1rem", + lg: "1.25rem", + xl: "1.5rem", + xxl: "2rem" +} as const; + +const Weights = { + thin: "100", + extralight: "200", + light: "300", + normal: "400", + medium: "500", + semibold: "600", + bold: "700", + extrabold: "800", +} as const; + +export function generateTextCss() { + let css = ""; + + for (const [size, value] of Object.entries(Sizes)) { + css += `.${textCls(size)}{font-size:${value};}`; + } + + for (const [weight, value] of Object.entries(Weights)) { + css += `.${textCls(weight)}{font-weight:${value};}`; + } + + return css; +} + +export type TextSize = keyof typeof Sizes; +export type TextWeight = keyof typeof Weights; +export type TextTag = "div" | "span" | "p" | `h${1 | 2 | 3 | 4 | 5 | 6}`; + +export type BaseTextProps = ComponentPropsWithoutRef & { + size?: TextSize; + weight?: TextWeight; + tag?: Tag; +}; + +export function BaseText(props: BaseTextProps): ReactNode { + const { + size = "md", + weight = "normal", + tag: Tag = "div", + children, + className, + ...restProps + } = props; + + return ( + + {children} + + ); +} + +// #region Old compability + +export const TextCompat: DiscordText = function TextCompat({ color, variant, ...restProps }) { + const newBaseTextProps = restProps as BaseTextProps; + + if (variant) { + const [left, right] = variant.split("/"); + if (left && right) { + const size = left.split("-").pop(); + newBaseTextProps.size = size as TextSize; + newBaseTextProps.weight = right as TextWeight; + } + } + + if (color) { + newBaseTextProps.style ??= {}; + newBaseTextProps.style.color = `var(--${color}, var(--text-default))`; + } + + return ; +}; + +// #endregion diff --git a/src/components/Button.css b/src/components/Button.css new file mode 100644 index 000000000..48c0dd5df --- /dev/null +++ b/src/components/Button.css @@ -0,0 +1,199 @@ +.vc-btn-base { + position: relative; + display: flex; + justify-content: center; + align-items: center; + max-width: 100%; + border: 1px solid transparent; + border-radius: var(--radius-sm, 8px); + font-family: var(--font-primary); + text-align: start; + transition: 50ms ease-in; + transition-property: background-color, color, border-color, opacity; + background: var(--control-background-secondary-default); + color: var(--text-default); + white-space: nowrap; + + &:hover { + transition: .15s ease-out; + } + + &:disabled { + opacity: .5; + pointer-events: none; + cursor: not-allowed; + } + + &:focus-visible { + /* stylelint-disable-next-line custom-property-pattern */ + box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--focus-primary, #00b0f4)); + } +} + +.vc-btn-min, +.vc-btn-xs { + padding: 3px 7px; + min-height: 22px; + min-width: unset; + font-size: 12px; + font-weight: 400; + line-height: 1.3333; +} + +.vc-btn-xs { + min-width: 60px; +} + +.vc-btn-small { + padding: 3px 11px; + min-height: 30px; + min-width: 60px; + font-size: 14px; + font-weight: 500; + line-height: 1.2857; +} + +.vc-btn-medium { + padding: 7px 15px; + min-height: 38px; + min-width: 100px; + font-size: 16px; + font-weight: 500; + line-height: 1.25; +} + +.vc-btn-primary { + background-color: var(--control-background-primary-default); + border-color: var(--control-border-primary-default); + color: var(--control-text-primary-default); + + &:hover { + background-color: var(--control-background-primary-hover); + border-color: var(--control-border-primary-hover); + color: var(--control-text-primary-hover); + } +} + +.vc-btn-secondary, +.vc-btn-link { + background-color: var(--control-background-secondary-default); + border-color: var(--control-border-secondary-default); + color: var(--control-text-secondary-default); + + &:hover { + background-color: var(--control-background-secondary-hover); + border-color: var(--control-border-secondary-hover); + color: var(--control-text-secondary-hover); + } +} + +.vc-btn-dangerPrimary { + background-color: var(--control-background-critical-primary-default); + border-color: var(--control-border-critical-primary-default); + color: var(--control-text-critical-primary-default); + + &:hover { + background-color: var(--control-background-critical-primary-hover); + border-color: var(--control-border-critical-primary-hover); + color: var(--control-text-critical-primary-hover); + } +} + +.vc-btn-dangerSecondary { + background-color: var(--control-background-critical-secondary-default); + border-color: var(--control-border-critical-secondary-default); + color: var(--control-text-critical-secondary-default); + + &:hover { + background-color: var(--control-background-critical-secondary-hover); + border-color: var(--control-border-critical-secondary-hover); + color: var(--control-text-critical-secondary-hover); + } +} + +.vc-btn-overlayPrimary { + background-color: var(--control-background-overlay-primary-default); + border-color: var(--control-border-overlay-primary-default); + color: var(--control-text-overlay-primary-default); + + &:hover { + background-color: var(--control-background-overlay-primary-hover); + border-color: var(--control-border-overlay-primary-hover); + color: var(--control-text-overlay-primary-hover); + } +} + +.vc-btn-positive { + background-color: var(--button-positive-background); + border-color: var(--button-positive-border); + color: var(--white); + + &:hover { + background-color: var(--button-positive-background-hover); + } +} + +.vc-btn-none { + background-color: transparent; + border-color: transparent; + color: var(--control-icon-icon-only-default); + + &:hover { + background-color: var(--control-background-icon-only-hover); + border-color: var(--control-border-icon-only-hover); + color: var(--control-icon-icon-only-hover); + } +} + +.vc-btn-link-icon { + width: 0.875em; + height: 0.875em; + margin-left: 8px; + flex-shrink: 0; +} + +.vc-text-btn-base { + display: inline-flex; + justify-content: center; + align-items: center; + gap: var(--space-4, 4px); + background: initial; + color: var(--text-default); + font-size: medium; + font-weight: 400; + margin: 0; + padding: 0; + text-align: start; + text-decoration: none; + max-width: 100%; + white-space: nowrap; + + &:disabled { + opacity: 0.5; + } + + &:hover { + text-decoration: underline; + } + + &:focus-visible { + /* stylelint-disable-next-line custom-property-pattern */ + box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--focus-primary, #00b0f4)); + } +} + +.vc-text-btn-primary { + color: var(--text-brand); +} + +.vc-text-btn-secondary { + color: var(--text-primary); +} + +.vc-text-btn-danger { + color: var(--text-feedback-critical, var(--text-danger)); +} + +.vc-text-btn-link { + color: var(--text-link); +} \ No newline at end of file diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 000000000..b3df68aba --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,107 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./Button.css"; + +import { classNameFactory } from "@api/Styles"; +import { classes } from "@utils/misc"; +import type { Button as DiscordButton } from "@vencord/discord-types"; +import type { ComponentPropsWithRef } from "react"; + +import { OpenExternalIcon } from "./Icons"; + +const btnCls = classNameFactory("vc-btn-"); +const textBtnCls = classNameFactory("vc-text-btn-"); + +export type ButtonVariant = + "primary" | "secondary" | "dangerPrimary" | "dangerSecondary" | "overlayPrimary" | "positive" | "link" | "none"; +export type ButtonSize = "min" | "xs" | "small" | "medium"; + +export type ButtonProps = ComponentPropsWithRef<"button"> & { + variant?: ButtonVariant; + size?: ButtonSize; +}; + +export function Button({ variant = "primary", size = "medium", children, className, ...restProps }: ButtonProps) { + return ( + + ); +} + +export type TextButtonVariant = "primary" | "secondary" | "danger" | "link"; + +export type TextButtonProps = ComponentPropsWithRef<"button"> & { + variant?: TextButtonVariant; +}; + +export function TextButton({ variant = "primary", className, ...restProps }: TextButtonProps) { + return ( + @@ -58,9 +58,9 @@ function ReloadRequiredCard({ required }: { required: boolean; }) { ) : ( <> - Plugin Management - Press the cog wheel or info icon to get more info on a plugin - Plugins with a cog wheel have settings you can modify! + Plugin Management + Press the cog wheel or info icon to get more info on a plugin + Plugins with a cog wheel have settings you can modify! )} @@ -87,10 +87,10 @@ function ExcludedPluginsList({ search }: { search: string; }) { }; return ( - + {matchingExcludedPlugins.length ? <> - Are you looking for: + Are you looking for:
    {matchingExcludedPlugins.map(([name, reason]) => (
  • @@ -101,7 +101,7 @@ function ExcludedPluginsList({ search }: { search: string; }) { : "No plugins meet the search criteria." } - + ); } @@ -245,15 +245,15 @@ function PluginSettings() { - + Filters - +
    -
    +
    onChange(e, index, "find")} - /> - onChange(e, index, "replace")} - /> - onChange(e, index, "onlyIfIncludes")} - /> - + + onChange(e, index, "find")} + /> + onChange(e, index, "replace")} + /> + onChange(e, index, "onlyIfIncludes")} + />