refactor(frontend): decompose ConfigPage into dedicated config components

- Extract tab components: JailsTab, ActionsTab, FiltersTab, JailFilesTab,
  GlobalTab, ServerTab, ConfFilesTab, RegexTesterTab, MapTab, ExportTab
- Add form components: JailFileForm, ActionForm, FilterForm
- Add AutoSaveIndicator, RegexList, configStyles, and barrel index
- ConfigPage now composes these components; greatly reduces file size
- Add tests: ConfigPage.test.tsx, useAutoSave.test.ts
This commit is contained in:
2026-03-13 13:48:09 +01:00
parent a0e8566ff8
commit 9b73f6719d
23 changed files with 4275 additions and 1828 deletions

View File

@@ -0,0 +1,186 @@
/**
* Shared makeStyles definitions for the config page and its components.
*
* All config tab components import `useConfigStyles` from this module
* so that visual changes need updating in only one place.
*/
import { makeStyles, tokens } from "@fluentui/react-components";
export const useConfigStyles = makeStyles({
page: {
padding: tokens.spacingVerticalXXL,
maxWidth: "1100px",
},
header: {
marginBottom: tokens.spacingVerticalL,
},
tabContent: {
marginTop: tokens.spacingVerticalL,
animationName: "fadeInUp",
animationDuration: tokens.durationNormal,
animationTimingFunction: tokens.curveDecelerateMid,
animationFillMode: "both",
},
section: {
marginBottom: tokens.spacingVerticalXL,
},
/** Card container for form sections — adds visual separation and depth. */
sectionCard: {
backgroundColor: tokens.colorNeutralBackground2,
borderRadius: tokens.borderRadiusMedium,
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalL}`,
boxShadow: tokens.shadow4,
marginBottom: tokens.spacingVerticalS,
},
/** Label row at the top of a sectionCard. */
sectionCardHeader: {
color: tokens.colorNeutralForeground2,
borderBottom: `1px solid ${tokens.colorNeutralStroke2}`,
paddingBottom: tokens.spacingVerticalS,
marginBottom: tokens.spacingVerticalM,
textTransform: "uppercase",
letterSpacing: "0.05em",
fontSize: tokens.fontSizeBase200,
fontWeight: tokens.fontWeightSemibold,
},
/** Monospace input with left brand-colour accent bar. */
codeInput: {
fontFamily: "monospace",
borderLeft: `3px solid ${tokens.colorBrandStroke1}`,
},
/** Applied to AccordionItem wrappers to get a hover background on headers. */
accordionItem: {
"& button:hover": {
backgroundColor: tokens.colorNeutralBackground1Hover,
},
},
/** Applied to AccordionItem wrappers that are currently expanded. */
accordionItemOpen: {
borderLeft: `3px solid ${tokens.colorBrandBackground}`,
},
fieldRow: {
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: tokens.spacingHorizontalM,
marginBottom: tokens.spacingVerticalS,
"@media (max-width: 900px)": {
gridTemplateColumns: "1fr",
},
},
fieldRowThree: {
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: tokens.spacingHorizontalM,
marginBottom: tokens.spacingVerticalS,
"@media (max-width: 900px)": {
gridTemplateColumns: "1fr 1fr",
},
"@media (max-width: 700px)": {
gridTemplateColumns: "1fr",
},
},
buttonRow: {
display: "flex",
gap: tokens.spacingHorizontalS,
marginTop: tokens.spacingVerticalM,
flexWrap: "wrap",
},
codeFont: {
fontFamily: "monospace",
fontSize: "0.85rem",
},
regexItem: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
marginBottom: tokens.spacingVerticalXS,
},
regexInput: {
flexGrow: "1",
fontFamily: "monospace",
borderLeft: `3px solid ${tokens.colorBrandStroke1}`,
},
logLine: {
padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
borderRadius: tokens.borderRadiusSmall,
fontFamily: "monospace",
fontSize: "0.8rem",
marginBottom: tokens.spacingVerticalXXS,
wordBreak: "break-all",
},
matched: {
backgroundColor: tokens.colorPaletteGreenBackground2,
},
notMatched: {
backgroundColor: tokens.colorNeutralBackground3,
},
previewArea: {
maxHeight: "400px",
overflowY: "auto",
padding: tokens.spacingHorizontalS,
border: `1px solid ${tokens.colorNeutralStroke1}`,
borderRadius: tokens.borderRadiusMedium,
marginTop: tokens.spacingVerticalS,
},
infoText: {
color: tokens.colorNeutralForeground3,
fontStyle: "italic",
},
/** Empty-state container: centred icon + message. */
emptyState: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: tokens.spacingVerticalM,
padding: `${tokens.spacingVerticalXXL} ${tokens.spacingHorizontalL}`,
color: tokens.colorNeutralForeground3,
textAlign: "center",
},
/** Auto-save status chip — for AutoSaveIndicator. */
autoSaveWrapper: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalXS,
fontSize: tokens.fontSizeBase200,
color: tokens.colorNeutralForeground2,
},
autoSaveSaved: {
opacity: "1",
transform: "scale(1)",
transition: `opacity ${tokens.durationNormal} ${tokens.curveDecelerateMid}, transform ${tokens.durationNormal} ${tokens.curveDecelerateMid}`,
},
autoSaveFadingOut: {
opacity: "0",
transition: `opacity ${tokens.durationNormal} ${tokens.curveDecelerateMid}`,
},
});
/**
* Global CSS keyframes injected once.
*
* ``makeStyles`` does not support top-level ``@keyframes``, so we inject them
* via a ``<style>`` element on first import. The function is idempotent.
*/
export function injectGlobalStyles(): void {
if (document.getElementById("bangui-global-styles")) return;
const style = document.createElement("style");
style.id = "bangui-global-styles";
style.textContent = `
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInScale {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
`;
document.head.appendChild(style);
}
// Inject keyframes on first module load (browser environment only).
if (typeof window !== "undefined") {
injectGlobalStyles();
}