diff --git a/src/components/ExpandableCard.css b/src/components/ExpandableCard.css
new file mode 100644
index 000000000..54ad0f712
--- /dev/null
+++ b/src/components/ExpandableCard.css
@@ -0,0 +1,23 @@
+.vc-expandable-card {
+ color: var(--text-default);
+}
+
+.vc-expandable-card-header {
+ display: flex;
+ cursor: pointer;
+ padding: .75em 1em;
+ margin: 4px;
+ border-radius: var(--radius-xs);
+
+ .vc-expandable-card[data-expanded="true"] & {
+ background: var(--card-secondary-bg);
+ }
+}
+
+.vc-expandable-card-icon {
+ margin-left: auto;
+}
+
+.vc-expandable-card-content {
+ padding: 0.5em 1em;
+}
\ No newline at end of file
diff --git a/src/components/ExpandableCard.tsx b/src/components/ExpandableCard.tsx
new file mode 100644
index 000000000..31ee11a24
--- /dev/null
+++ b/src/components/ExpandableCard.tsx
@@ -0,0 +1,45 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2026 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./ExpandableCard.css";
+
+import { classes } from "@utils/misc";
+import { Clickable, useState } from "@webpack/common";
+import { PropsWithChildren } from "react";
+
+import { Card } from "./Card";
+import { DownArrow, RightArrow } from "./Icons";
+
+export type ExpandableSectionProps = PropsWithChildren<{
+ renderContent: () => React.ReactNode;
+ className?: string;
+ initialExpanded?: boolean;
+}>;
+
+/**
+ * A card component that can expand and collapse to show/hide content. The header (props.children) is always visible, and the content (props.renderContent) is only visible when expanded.
+ */
+export function ExpandableSection({ children, renderContent: Content, className, initialExpanded = false }: ExpandableSectionProps) {
+ const [expanded, setExpanded] = useState(initialExpanded);
+
+ const Icon = expanded ? DownArrow : RightArrow;
+
+ return (
+
+ setExpanded(c => !c)} >
+ {children}
+
+
+
+ {expanded
+ ?
+
+
+ : null
+ }
+
+ );
+}
diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx
index 6e39f3f49..eb36ede95 100644
--- a/src/components/Icons.tsx
+++ b/src/components/Icons.tsx
@@ -571,7 +571,7 @@ export function CloudUploadIcon(props: IconProps) {
);
}
-export const ClockIcon = (props?: any) => {
+export function ClockIcon(props: IconProps) {
return (
{
/>
);
-};
+}
+
+export function DownArrow(props: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function RightArrow(props: IconProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx
index 8122ab395..8e70d85c8 100644
--- a/src/plugins/textReplace/index.tsx
+++ b/src/plugins/textReplace/index.tsx
@@ -16,22 +16,31 @@
* along with this program. If not, see .
*/
+import "./styles.css";
+
import { definePluginSettings } from "@api/Settings";
+import { Button } from "@components/Button";
+import { ExpandableSection } from "@components/ExpandableCard";
import { Flex } from "@components/Flex";
-import { DeleteIcon } from "@components/Icons";
+import { HeadingSecondary } from "@components/Heading";
+import { Paragraph } from "@components/Paragraph";
+import { Span } from "@components/Span";
+import { TooltipContainer } from "@components/TooltipContainer";
import { Devs } from "@utils/constants";
+import { classNameFactory } from "@utils/css";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
-import { Button, Forms, React, TextInput, useState } from "@webpack/common";
+import { React, TextInput, useState } from "@webpack/common";
-const STRING_RULES_KEY = "TextReplace_rulesString";
-const REGEX_RULES_KEY = "TextReplace_rulesRegex";
+const cl = classNameFactory("vc-textReplace-");
type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
interface TextReplaceProps {
title: string;
+ description: string;
rulesArray: Rule[];
+ isRegex?: boolean;
}
const makeEmptyRule: () => Rule = () => ({
@@ -49,15 +58,18 @@ const settings = definePluginSettings({
return (
<>
+
-
>
);
}
@@ -116,21 +128,32 @@ function Input({ initialValue, onChange, placeholder }: {
);
}
-function TextReplace({ title, rulesArray }: TextReplaceProps) {
- const isRegexRules = title === "Using Regex";
+function TextRow({ label, description, value, onChange }: { label: string; description: string; value: string; onChange(value: string): void; }) {
+ return (
+ <>
+
+ {label}
+
+
+ >
+ );
+}
- async function onClickRemove(index: number) {
- if (index === rulesArray.length - 1) return;
+const isEmptyRule = (rule: Rule) => !rule.find;
+
+function TextReplace({ title, description, rulesArray, isRegex = false }: TextReplaceProps) {
+ function onClickRemove(index: number) {
rulesArray.splice(index, 1);
}
- async function onChange(e: string, index: number, key: string) {
- if (index === rulesArray.length - 1) {
- rulesArray.push(makeEmptyRule());
- }
-
+ function onChange(e: string, index: number, key: string) {
rulesArray[index][key] = e;
+ // If a rule is empty after editing and is not the last rule, remove it
if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) {
rulesArray.splice(index, 1);
}
@@ -138,49 +161,61 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
return (
<>
- {title}
+
+
{title}
+
{description}
+
- {
- rulesArray.map((rule, index) =>
-
-
- onChange(e, index, "find")}
- />
- onChange(e, index, "replace")}
- />
- onChange(e, index, "onlyIfIncludes")}
- />
+ {rulesArray.map((rule, index) =>
+ (
+ <>
+
+ onChange(e, index, "find")}
+ />
+ onChange(e, index, "replace")}
+ />
+ onChange(e, index, "onlyIfIncludes")}
+ />
+
+ {isRegex && renderFindError(rule.find)}
-
- {isRegexRules && renderFindError(rule.find)}
-
- )
- }
+ >
+ )}
+ >
+
+ {isEmptyRule(rule)
+ ? `Empty Rule ${index + 1}`
+ : `Rule ${index + 1} - ${rule.find}`
+ }
+
+
+ )}
+
>
);
@@ -188,12 +223,15 @@ function TextReplace({ title, rulesArray }: TextReplaceProps) {
function TextReplaceTesting() {
const [value, setValue] = useState("");
+
return (
- <>
- Test Rules
-
-
- >
+
+ Rule Tester
+
+
+
+
+
);
}
diff --git a/src/plugins/textReplace/styles.css b/src/plugins/textReplace/styles.css
new file mode 100644
index 000000000..51a65fe1a
--- /dev/null
+++ b/src/plugins/textReplace/styles.css
@@ -0,0 +1,11 @@
+.vc-textReplace-input-grid {
+ display: grid;
+ grid-template-columns: max-content 1fr;
+ gap: 0.25em 1em;
+ align-items: center;
+}
+
+.vc-textReplace-delete-button {
+ width: 100%;
+ margin-block: 0.75em 0.5em
+}
\ No newline at end of file