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:
186
frontend/src/components/config/configStyles.ts
Normal file
186
frontend/src/components/config/configStyles.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user