remake and improve Card/Flex components

This commit is contained in:
Vendicated 2025-11-19 02:21:24 +01:00
parent 78a2add1a5
commit 3452a722b4
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
21 changed files with 163 additions and 110 deletions

20
src/components/Card.css Normal file
View file

@ -0,0 +1,20 @@
.vc-card-base {
background: var(--card-primary-bg);
border-radius: var(--radius-sm, 8px)
}
.vc-card-normal {
border: 1px solid var(--border-subtle);
}
.vc-card-warning {
border: 1px solid var(--info-warning-foreground);
}
.vc-card-danger {
border: 1px solid var(--info-danger-foreground);
}
.vc-card-defaultPadding {
padding: 1em;
}

31
src/components/Card.tsx Normal file
View file

@ -0,0 +1,31 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./Card.css";
import { classNameFactory } from "@api/Styles";
import { classes } from "@utils/misc";
import { ComponentPropsWithRef } from "react";
const cl = classNameFactory("vc-card-");
export interface CardProps extends ComponentPropsWithRef<"div"> {
variant?: "normal" | "warning" | "danger";
/** Add a default padding of 1em to the card. This is implied if no className prop is passed */
defaultPadding?: boolean;
}
export function Card({ variant = "normal", defaultPadding, children, className, ...restProps }: CardProps) {
const addDefaultPadding = defaultPadding != null
? defaultPadding
: !className;
return (
<div className={classes(cl("base", variant, addDefaultPadding && "defaultPadding"), className)} {...restProps}>
{children}
</div>
);
}

View file

@ -16,22 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { React } from "@webpack/common";
import type { CSSProperties, HTMLAttributes } from "react";
export interface FlexProps extends HTMLAttributes<HTMLDivElement> {
flexDirection?: CSSProperties["flexDirection"];
gap?: CSSProperties["gap"];
justifyContent?: CSSProperties["justifyContent"];
alignItems?: CSSProperties["alignItems"];
}
export function Flex({ flexDirection, gap = "1em", justifyContent, alignItems, children, style, ...restProps }: FlexProps) {
style ??= {};
Object.assign(style, {
display: "flex",
flexDirection,
gap,
justifyContent,
alignItems,
});
export function Flex(props: React.PropsWithChildren<{
flexDirection?: React.CSSProperties["flexDirection"];
style?: React.CSSProperties;
className?: string;
} & React.HTMLProps<HTMLDivElement>>) {
props.style ??= {};
props.style.display = "flex";
// TODO(ven): Remove me, what was I thinking??
props.style.gap ??= "1em";
props.style.flexDirection ||= props.flexDirection;
delete props.flexDirection;
return (
<div {...props}>
{props.children}
<div style={style} {...restProps}>
{children}
</div>
);
}

View file

@ -5,6 +5,8 @@
*/
export * from "./BaseText";
export * from "./Button";
export * from "./Card";
export * from "./CheckedTextInput";
export * from "./CodeBlock";
export * from "./Divider";

View file

@ -7,7 +7,7 @@
import "./QuickAction.css";
import { classNameFactory } from "@api/Styles";
import { Card } from "@webpack/common";
import { Card } from "@components/Card";
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
const cl = classNameFactory("vc-settings-quickActions-");

View file

@ -19,8 +19,9 @@
import "./SpecialCard.css";
import { classNameFactory } from "@api/Styles";
import { Card } from "@components/Card";
import { Divider } from "@components/Divider";
import { Card, Clickable, Forms, React } from "@webpack/common";
import { Clickable, Forms } from "@webpack/common";
import type { PropsWithChildren } from "react";
const cl = classNameFactory("vc-special-");

View file

@ -22,6 +22,7 @@ import * as DataStore from "@api/DataStore";
import { isPluginEnabled } from "@api/PluginManager";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Card } from "@components/Card";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
import { HeadingTertiary } from "@components/Heading";
@ -32,7 +33,7 @@ import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter, useCleanupEffect } from "@utils/react";
import { Alerts, Button, Card, lodash, Parser, React, Select, TextInput, Tooltip, useMemo, useState } from "@webpack/common";
import { Alerts, Button, lodash, Parser, React, Select, TextInput, Tooltip, useMemo, useState } from "@webpack/common";
import { JSX } from "react";
import Plugins, { ExcludedPlugins } from "~plugins";
@ -44,7 +45,7 @@ export const logger = new Logger("PluginSettings", "#a6d189");
function ReloadRequiredCard({ required }: { required: boolean; }) {
return (
<Card className={classes(cl("info-card"), required && "vc-warning-card")}>
<Card variant={required ? "warning" : "normal"} className={cl("info-card")}>
{required
? (
<>

View file

@ -15,24 +15,6 @@
flex-direction: row;
}
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
}
.vc-warning-card {
padding: 1em;
background: var(--info-warning-background);
border: 1px solid var(--info-warning-foreground);
color: var(--info-warning-foreground);
}
.vc-backup-restore-card {
background-color: var(--info-warning-background);
border-color: var(--info-warning-foreground);
color: var(--info-warning-foreground);
}
.vc-settings-theme-links {
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
display: inline-block !important;

View file

@ -17,49 +17,48 @@
*/
import { downloadSettingsBackup, uploadSettingsBackup } from "@api/SettingsSync/offline";
import { Card } from "@components/Card";
import { Flex } from "@components/Flex";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { Button, Card, Text } from "@webpack/common";
import { Button, Text } from "@webpack/common";
function BackupAndRestoreTab() {
return (
<SettingsTab title="Backup & Restore">
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column">
<strong>Warning</strong>
<span>Importing a settings file will overwrite your current settings.</span>
<Flex flexDirection="column" gap="0.5em">
<Card variant="warning">
<Heading tag="h4">Warning</Heading>
<Paragraph>Importing a settings file will overwrite your current settings.</Paragraph>
</Card>
<Text variant="text-md/normal" className={Margins.bottom8}>
You can import and export your Vencord settings as a JSON file.
This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord.
</Text>
<Heading tag="h4">Settings Export contains:</Heading>
<Text variant="text-md/normal" className={Margins.bottom8}>
<ul>
<li>&mdash; Custom QuickCSS</li>
<li>&mdash; Theme Links</li>
<li>&mdash; Plugin Settings</li>
</ul>
</Text>
<Flex>
<Button onClick={() => uploadSettingsBackup()}>
Import Settings
</Button>
<Button onClick={downloadSettingsBackup}>
Export Settings
</Button>
</Flex>
</Card>
<Text variant="text-md/normal" className={Margins.bottom8}>
You can import and export your Vencord settings as a JSON file.
This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord.
</Text>
<Text variant="text-md/normal" className={Margins.bottom8}>
Settings Export contains:
<ul>
<li>&mdash; Custom QuickCSS</li>
<li>&mdash; Theme Links</li>
<li>&mdash; Plugin Settings</li>
</ul>
</Text>
<Flex>
<Button
onClick={() => uploadSettingsBackup()}
size={Button.Sizes.SMALL}
>
Import Settings
</Button>
<Button
onClick={downloadSettingsBackup}
size={Button.Sizes.SMALL}
>
Export Settings
</Button>
</Flex>
</SettingsTab>
</SettingsTab >
);
}

View file

@ -50,7 +50,7 @@ export function CspErrorCard() {
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
return (
<ErrorCard className="vc-settings-card">
<ErrorCard className={Margins.bottom16}>
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>

View file

@ -7,6 +7,8 @@
import { isPluginEnabled } from "@api/PluginManager";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Card } from "@components/Card";
import { Flex } from "@components/Flex";
import { FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
@ -14,7 +16,7 @@ import { openPluginModal } from "@components/settings/tabs/plugins/PluginModal";
import { UserThemeHeader } from "@main/themes";
import ClientThemePlugin from "@plugins/clientTheme";
import { findLazy } from "@webpack";
import { Card, Forms, useEffect, useRef, useState } from "@webpack/common";
import { Forms, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";
import { ThemeCard } from "./ThemeCard";
@ -82,8 +84,8 @@ export function LocalThemesTab() {
}
return (
<>
<Card className="vc-settings-card">
<Flex flexDirection="column" gap="1em">
<Card>
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
@ -94,7 +96,7 @@ export function LocalThemesTab() {
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
</Card>
<Card className="vc-settings-card">
<Card>
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
@ -167,6 +169,6 @@ export function LocalThemesTab() {
))}
</div>
</section>
</>
</Flex>
);
}

View file

@ -5,9 +5,9 @@
*/
import { useSettings } from "@api/Settings";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { Card, Forms, TextArea, useState } from "@webpack/common";
import { Card } from "@components/Card";
import { Flex } from "@components/Flex";
import { Forms, TextArea, useState } from "@webpack/common";
export function OnlineThemesTab() {
const settings = useSettings(["themeLinks"]);
@ -26,14 +26,14 @@ export function OnlineThemesTab() {
}
return (
<>
<Card className={classes("vc-warning-card", Margins.bottom16)}>
<Flex flexDirection="column" gap="1em">
<Card variant="warning" defaultPadding>
<Forms.FormText size="md">
This section is for advanced users. If you are having difficulties using it, use the
Local Themes tab instead.
</Forms.FormText>
</Card>
<Card className="vc-settings-card">
<Card>
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
@ -52,6 +52,6 @@ export function OnlineThemesTab() {
rows={10}
/>
</section>
</>
</Flex>
);
}

View file

@ -18,10 +18,11 @@
import "./styles.css";
import { Card } from "@components/Card";
import { Link } from "@components/Link";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { getStylusWebStoreUrl } from "@utils/web";
import { Card, Forms, React, TabBar, useState } from "@webpack/common";
import { Forms, React, TabBar, useState } from "@webpack/common";
import { CspErrorCard } from "./CspErrorCard";
import { LocalThemesTab } from "./LocalThemesTab";
@ -69,7 +70,7 @@ function ThemesTab() {
function UserscriptThemesTab() {
return (
<SettingsTab title="Themes">
<Card className="vc-settings-card">
<Card variant="danger">
<Forms.FormTitle tag="h5">Themes are not supported on the Userscript!</Forms.FormTitle>
<Forms.FormText>

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Card } from "@components/Card";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
@ -11,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { changes, checkForUpdates, update, updateError } from "@utils/updater";
import { Alerts, Button, Card, Forms, React, Toasts, useState } from "@webpack/common";
import { Alerts, Button, Forms, React, Toasts, useState } from "@webpack/common";
import { runWithDispatch } from "./runWithDispatch";
@ -30,7 +31,7 @@ export function HashLink({ repo, hash, disabled = false }: { repo: string, hash:
export function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
return (
<Card style={{ padding: "0 0.5em" }}>
<Card style={{ padding: "0 0.5em" }} defaultPadding={false}>
{updates.map(({ hash, author, message }) => (
<div
key={hash}

View file

@ -46,7 +46,7 @@ function Updater() {
return (
<SettingsTab title="Vencord Updater">
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Forms.FormTitle tag="h5" className={Margins.bottom16}>Updater Settings</Forms.FormTitle>
<FormSwitch
title="Automatically update"

View file

@ -19,6 +19,7 @@
import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import { Card } from "@components/Card";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
@ -34,7 +35,7 @@ import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Channel } from "@vencord/discord-types";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { Alerts, Button, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { JSX } from "react";
import gitHash from "~git-hash";
@ -320,7 +321,7 @@ export default definePlugin({
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
return (
<Card className={`vc-warning-card ${Margins.top8}`}>
<Card variant="warning" className={Margins.top8} defaultPadding>
Please do not private message Vencord plugin developers for support!
<br />
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}

View file

@ -22,6 +22,7 @@ import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { Divider } from "@components/Divider";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { copyWithToast, fetchUserProfile } from "@utils/discord";
import { Margins } from "@utils/margins";
@ -30,7 +31,7 @@ import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { User, UserProfile } from "@vencord/discord-types";
import { findComponentByCodeLazy } from "@webpack";
import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { Button, ColorPicker, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import virtualMerge from "virtual-merge";
interface Colors {
@ -119,23 +120,19 @@ function SettingsAboutComponent() {
<Forms.FormText>
After enabling this plugin, you will see custom colors in
the profiles of other people using compatible plugins.{" "}
<br />
To set your own colors:
</Forms.FormText>
<Forms.FormText className={Margins.top8}>
<strong>To set your own profile theme colors:</strong>
<ul>
<li>
use the color pickers below to choose your colors
</li>
<li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li>
</ul><br />
<li>&mdash; use the color pickers below to choose your colors</li>
<li>&mdash; click the "Copy 3y3" button</li>
<li>&mdash; paste the invisible text anywhere in your bio</li>
</ul>
<Divider
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
<Flex
direction={Flex.Direction.HORIZONTAL}
style={{ gap: "1rem" }}
>
<Flex gap="1em">
<ColorPicker
color={color1}
label={
@ -171,6 +168,7 @@ function SettingsAboutComponent() {
}}
color={Button.Colors.PRIMARY}
size={Button.Sizes.XLARGE}
style={{ marginBottom: "auto" }}
>
Copy 3y3
</Button>

View file

@ -6,9 +6,10 @@
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Flex, Menu } from "@webpack/common";
import { Menu } from "@webpack/common";
const DefaultEngines = {
Google: "https://www.google.com/search?q=",
@ -93,7 +94,7 @@ function makeSearchItem(src: string) {
key={key}
id={key}
label={
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
<Flex gap="0.5em" alignItems="center">
<img
style={{
borderRadius: "50%"

View file

@ -84,7 +84,7 @@ function openViewRawModal(json: string, type: string, msgContent?: string) {
</div>
</ModalContent >
<ModalFooter>
<Flex cellSpacing={10}>
<Flex>
<Button onClick={() => copyWithToast(json, `${type} data copied to clipboard!`)}>
Copy {type} JSON
</Button>

View file

@ -19,6 +19,7 @@
import "./styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Card } from "@components/Card";
import { Microphone } from "@components/Icons";
import { Link } from "@components/Link";
import { Paragraph } from "@components/Paragraph";
@ -31,7 +32,7 @@ import { chooseFile } from "@utils/web";
import { CloudUpload as TCloudUpload } from "@vencord/discord-types";
import { CloudUploadPlatform } from "@vencord/discord-types/enums";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { Button, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./DesktopRecorder";
@ -226,7 +227,7 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) {
)}
{isUnsupportedFormat && (
<Card className={`vc-warning-card ${Margins.top16}`}>
<Card variant="warning" className={Margins.top16} defaultPadding>
<Forms.FormText>Voice Messages have to be OggOpus to be playable on iOS. This file is <code>{blob.type}</code> so it will not be playable on iOS.</Forms.FormText>
<Forms.FormText className={Margins.top8}>

View file

@ -30,7 +30,9 @@ import { waitForComponent } from "./internal";
export const Forms = {
// TODO: Stop using this and use Heading/Paragraph directly
/** @deprecated use Heading from Vencord */
FormTitle: Heading,
/** @deprecated use Paragraph from Vencord */
FormText: Paragraph,
/** @deprecated don't use this */
FormSection: "section" as never, // Backwards compat since Vesktop uses this
@ -39,12 +41,15 @@ export const Forms = {
};
// TODO: Stop using this and use Paragraph/Span directly
/** @deprecated use Paragraph, Span, or BaseText from Vencord */
export const Text = TextCompat;
/** @deprecated use Button from Vencord */
export const Button = ButtonCompat;
/** @deprecated Use FormSwitch from Vencord */
export const Switch = FormSwitchCompat as never;
export const Card = waitForComponent<t.Card>("Card", filters.componentByCode(".editable),", ".outline:"));
/** @deprecated Use Card from Vencord */
export const Card = waitForComponent<never>("Card", filters.componentByCode(".editable),", ".outline:"));
export const Checkbox = waitForComponent<t.Checkbox>("Checkbox", filters.componentByCode(".checkboxWrapperDisabled:"));
const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", {
@ -55,6 +60,7 @@ const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", {
TooltipContainer: t.TooltipContainer;
};
// TODO: if these finds break, they should just return their children
export const Tooltip = LazyComponent(() => Tooltips.Tooltip);
export const TooltipContainer = LazyComponent(() => Tooltips.TooltipContainer);
@ -67,6 +73,7 @@ export const Popout = waitForComponent<t.Popout>("Popout", filters.componentByCo
export const Dialog = waitForComponent<t.Dialog>("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1'));
export const TabBar = waitForComponent("TabBar", filters.componentByCode("ref:this.tabBarRef,className:"));
export const Paginator = waitForComponent<t.Paginator>("Paginator", filters.componentByCode('rel:"prev",children:'));
// TODO: remake this component
export const Clickable = waitForComponent<t.Clickable>("Clickable", filters.componentByCode("this.context?this.renderNonInteractive():"));
export const Avatar = waitForComponent<t.Avatar>("Avatar", filters.componentByCode(".size-1.375*"));
@ -106,7 +113,6 @@ waitFor(m => {
export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", filters.componentByCode("MASKED_LINK)"));
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.componentByCode("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const OAuth2AuthorizeModal = waitForComponent("OAuth2AuthorizeModal", filters.componentByCode(".authorize,children:", ".contentBackground"));
export const Animations = mapMangledModuleLazy(".assign({colorNames:", {