Add terminal text color configuration modal

Introduces ModalConfig component for customizing terminal text color, accessible from DragTabs via a new settings icon. TerminalXTerm now reads the color from localStorage, allowing user-selected colors to persist across sessions.
This commit is contained in:
nguyentrungthat 2025-12-01 12:03:58 +07:00
parent b012a26c47
commit 9c38adb69e
3 changed files with 193 additions and 4 deletions

View File

@ -31,6 +31,7 @@ import {
IconChevronRight,
IconEdit,
IconLogout,
IconSettings,
IconSettingsPlus,
IconUsersGroup,
} from "@tabler/icons-react";
@ -38,6 +39,7 @@ import classes from "./Component.module.css";
import type { TStation, TUser } from "../untils/types";
import type { Socket } from "socket.io-client";
import ModalHistory from "./ModalHistory";
import ModalConfig from "./ModalConfig";
interface DraggableTabsProps {
tabsData: TStation[];
@ -126,6 +128,7 @@ export default function DraggableTabs({
const [isChangeTab, setIsChangeTab] = useState<boolean>(false);
const [isSetActive, setIsSetActive] = useState<boolean>(false);
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState<boolean>(false);
const [openConfig, setOpenConfig] = useState<boolean>(false);
const sensors = useSensors(useSensor(PointerSensor));
@ -229,7 +232,15 @@ export default function DraggableTabs({
w={w}
>
<Flex justify={"space-between"}>
<Flex></Flex>
<Flex style={{ marginTop: "8px" }}>
<ActionIcon
title="Setting"
variant="outline"
onClick={() => setOpenConfig(true)}
>
<IconSettings />
</ActionIcon>
</Flex>
<Tabs.List className={classes.list}>
<SortableContext
items={tabs}
@ -354,6 +365,14 @@ export default function DraggableTabs({
socket={socket}
stationIds={tabs.map((el) => el.id)}
/>
<ModalConfig
opened={openConfig}
onClose={() => setOpenConfig(false)}
onSave={() => {
onChange(active);
}}
/>
</DndContext>
);
}

View File

@ -0,0 +1,161 @@
import { useEffect, useRef, useState } from "react";
import { Modal, Button, ColorPicker, Group, Grid } from "@mantine/core";
import { Terminal } from "xterm";
import { FitAddon } from "@xterm/addon-fit";
interface Props {
opened: boolean;
onClose: () => void;
onSave: () => void;
}
export default function ModalConfig({ opened, onClose, onSave }: Props) {
const [color, setColor] = useState("#41ee4a");
const [loaded, setLoaded] = useState(false);
const xtermRef = useRef<HTMLDivElement>(null);
const terminal = useRef<Terminal>(null);
const fitRef = useRef<FitAddon>(null);
useEffect(() => {
// Load saved color from localStorage
const saved = localStorage.getItem("terminal-text-color");
if (saved) setColor(saved);
}, []);
const handleSave = () => {
localStorage.setItem("terminal-text-color", color);
onClose();
onSave();
};
useEffect(() => {
if (!loaded || fitRef.current || terminal.current || !xtermRef.current)
return;
const textColor = localStorage.getItem("terminal-text-color") || "#41ee4a";
terminal.current = new Terminal({
disableStdin: true,
cursorBlink: true,
convertEol: true,
fontFamily: "Consolas, 'Courier New', monospace",
fontSize: 12,
theme: {
background: "#000000",
foreground: textColor,
cursor: "#ffffff",
},
rows: 24,
cols: 80,
});
const fitAddon = new FitAddon();
fitRef.current = fitAddon;
terminal.current.loadAddon(fitAddon);
if (xtermRef.current) terminal.current.open(xtermRef.current);
terminal.current?.write(
"Change color \nChange color\nChange color\nChange color"
);
fitAddon.fit();
}, [loaded, xtermRef]);
useEffect(() => {
if (terminal.current && terminal.current.options.theme) {
terminal.current.options.theme = {
background: "#000000",
foreground: color,
cursor: "#ffffff",
};
}
}, [color]);
useEffect(() => {
if (opened) {
setTimeout(() => {
setLoaded(true);
}, 100);
} else {
setLoaded(false);
fitRef.current = null;
terminal.current = null;
const saved = localStorage.getItem("terminal-text-color");
if (saved) setColor(saved);
}
}, [opened]);
return (
<Modal
size={"xl"}
style={{ position: "absolute", left: 0 }}
opened={opened}
onClose={onClose}
title="Terminal Text Color"
>
<Grid>
<Grid.Col span={6}>
<Group>
<ColorPicker
format="hex"
value={color}
onChange={setColor}
fullWidth
withPicker={false}
swatches={[
"#ffffff",
"#41ee4a",
"#fa5252",
"#e64980",
"#be4bdb",
"#7950f2",
"#4c6ef5",
"#228be6",
"#15aabf",
"#12b886",
"#40c057",
"#82c91e",
"#fab005",
"#fd7e14",
]}
/>
<Button fullWidth onClick={handleSave}>
Save
</Button>
</Group>
</Grid.Col>
<Grid.Col span={6}>
<div
style={{
width: "100%",
height: "100%",
backgroundColor: "black",
paddingBottom: "4px",
maxHeight: "220px",
}}
>
<div
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
ref={xtermRef}
style={{
width: "100%",
paddingLeft: "10px",
paddingBottom: "10px",
fontSize: "12px",
maxHeight: "220px",
height: "220px",
padding: "4px",
paddingRight: 0,
}}
onDoubleClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
/>
</div>
</Grid.Col>
</Grid>
</Modal>
);
}

View File

@ -56,8 +56,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
const [isInit, setIsInit] = useState<boolean>(false);
useEffect(() => {
if (!cliOpened || fitRef.current) return;
if (!cliOpened || fitRef.current || terminal.current) return;
const textColor = localStorage.getItem("terminal-text-color") || "#41ee4a";
terminal.current = new Terminal({
disableStdin: isDisabled,
cursorBlink: true,
@ -66,7 +66,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
fontSize: fontSize,
theme: {
background: "#000000",
foreground: "#41ee4a",
foreground: textColor,
cursor: "#ffffff",
},
rows: 24,
@ -160,6 +160,15 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
setIsInit(true);
if (!miniSize && !isDisabled) terminal.current?.focus();
terminal.current.scrollToBottom();
if (terminal.current.options.theme) {
const textColor =
localStorage.getItem("terminal-text-color") || "#41ee4a";
terminal.current.options.theme = {
background: "#000000",
foreground: textColor,
cursor: "#ffffff",
};
}
}
if (fitRef.current) fitRef.current?.fit();
setLoading(true);