Replace inline frontend styles with makeStyles and design tokens
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
Reference in New Issue
Block a user