Replace inline frontend styles with makeStyles and design tokens

This commit is contained in:
2026-04-19 12:04:24 +02:00
parent 91269448d0
commit d99d6bd119
16 changed files with 344 additions and 265 deletions

View File

@@ -14,6 +14,8 @@ import {
Button,
Spinner,
Text,
makeStyles,
mergeClasses,
tokens,
} from "@fluentui/react-components";
import { Checkmark16Regular } from "@fluentui/react-icons";
@@ -33,6 +35,32 @@ export interface AutoSaveIndicatorProps {
/** Fade-out delay after "saved" status in milliseconds. */
const SAVED_FADE_DELAY_MS = 2000;
const useStyles = makeStyles({
root: {
display: "inline-flex",
alignItems: "center",
gap: tokens.spacingHorizontalXS,
minWidth: "80px",
},
savingText: {
color: tokens.colorNeutralForeground2,
},
errorText: {
color: tokens.colorPaletteRedForeground3,
},
savedMotion: {
transition: `opacity ${tokens.durationNormal} ${tokens.curveDecelerateMid}, transform ${tokens.durationNormal} ${tokens.curveDecelerateMid}`,
animationName: "fadeInScale",
animationDuration: tokens.durationFast,
animationTimingFunction: tokens.curveDecelerateMid,
animationFillMode: "both",
},
savedHidden: {
opacity: 0,
transform: "scale(0.95)",
},
});
/**
* Compact inline indicator for auto-save state.
*
@@ -44,6 +72,7 @@ export function AutoSaveIndicator({
errorText,
onRetry,
}: AutoSaveIndicatorProps): React.JSX.Element {
const styles = useStyles();
const [fadingOut, setFadingOut] = useState(false);
// Trigger the fade-out transition 2 s after the saved state is reached.
@@ -62,20 +91,11 @@ export function AutoSaveIndicator({
// Always render the aria-live region so screen readers track changes.
return (
<span
aria-live="polite"
role="status"
style={{
display: "inline-flex",
alignItems: "center",
gap: tokens.spacingHorizontalXS,
minWidth: 80,
}}
>
<span aria-live="polite" role="status" className={styles.root}>
{status === "saving" && (
<>
<Spinner size="extra-tiny" />
<Text size={200} style={{ color: tokens.colorNeutralForeground2 }}>
<Text size={200} className={styles.savingText}>
Saving
</Text>
</>
@@ -83,15 +103,10 @@ export function AutoSaveIndicator({
{status === "saved" && (
<span
style={{
opacity: fadingOut ? 0 : 1,
transform: fadingOut ? "scale(0.95)" : "scale(1)",
transition: `opacity ${tokens.durationNormal} ${tokens.curveDecelerateMid}, transform ${tokens.durationNormal} ${tokens.curveDecelerateMid}`,
animationName: fadingOut ? undefined : "fadeInScale",
animationDuration: tokens.durationFast,
animationTimingFunction: tokens.curveDecelerateMid,
animationFillMode: "both",
}}
className={mergeClasses(
styles.savedMotion,
fadingOut ? styles.savedHidden : undefined,
)}
>
<Badge
appearance="tint"
@@ -106,10 +121,7 @@ export function AutoSaveIndicator({
{status === "error" && (
<>
<Text
size={200}
style={{ color: tokens.colorPaletteRedForeground3 }}
>
<Text size={200} className={styles.errorText}>
{errorText ?? "Save failed."}
</Text>
{onRetry && (

View File

@@ -17,6 +17,7 @@ import {
MessageBarBody,
Spinner,
Textarea,
makeStyles,
tokens,
} from "@fluentui/react-components";
import { AutoSaveIndicator } from "./AutoSaveIndicator";
@@ -37,6 +38,37 @@ export interface RawConfigSectionProps {
/** Minimum visible rows for the monospace text area. */
const MIN_ROWS = 15;
const useStyles = makeStyles({
headerText: {
fontWeight: tokens.fontWeightSemibold,
},
loadingRow: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
padding: tokens.spacingVerticalM,
},
loadingText: {
color: tokens.colorNeutralForeground3,
},
textArea: {
fontFamily: tokens.fontFamilyMonospace,
fontSize: tokens.fontSizeBase200,
width: "100%",
borderLeft: `3px solid ${tokens.colorBrandStroke1}`,
marginBottom: tokens.spacingVerticalS,
},
controlsRow: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
marginTop: tokens.spacingVerticalXS,
},
errorBar: {
marginBottom: tokens.spacingVerticalS,
},
});
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
@@ -52,6 +84,7 @@ export function RawConfigSection({
saveContent,
label = "Raw Configuration",
}: RawConfigSectionProps): React.JSX.Element {
const styles = useStyles();
/** Raw text content; null means not yet loaded. */
const [content, setContent] = useState<string | null>(null);
const [localText, setLocalText] = useState("");
@@ -122,27 +155,20 @@ export function RawConfigSection({
<Accordion collapsible onToggle={handleExpand}>
<AccordionItem value="raw">
<AccordionHeader>
<span style={{ fontWeight: tokens.fontWeightSemibold }}>{label}</span>
<span className={styles.headerText}>{label}</span>
</AccordionHeader>
<AccordionPanel>
{fetchLoading && (
<div
style={{
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
padding: tokens.spacingVerticalM,
}}
>
<div className={styles.loadingRow}>
<Spinner size="tiny" />
<span style={{ color: tokens.colorNeutralForeground3 }}>
<span className={styles.loadingText}>
Loading
</span>
</div>
)}
{fetchError && (
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalS }}>
<MessageBar intent="error" className={styles.errorBar}>
<MessageBarBody>{fetchError}</MessageBarBody>
</MessageBar>
)}
@@ -156,23 +182,10 @@ export function RawConfigSection({
}}
resize="vertical"
rows={MIN_ROWS}
style={{
fontFamily: "monospace",
fontSize: "0.85rem",
width: "100%",
borderLeft: `3px solid ${tokens.colorBrandStroke1}`,
marginBottom: tokens.spacingVerticalS,
}}
className={styles.textArea}
aria-label={label}
/>
<div
style={{
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS,
marginTop: tokens.spacingVerticalXS,
}}
>
<div className={styles.controlsRow}>
<Button
appearance="primary"
onClick={() => {