UI redesign staging (#6149)

Co-authored-by: Reece Browne <[email protected]>
Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
EthanHealy01
2026-05-15 15:36:50 +01:00
committed by GitHub
co-authored by Reece Browne James Brunton
parent beb99e273b
commit c731d5fd5d
173 changed files with 7290 additions and 4269 deletions
@@ -1,75 +0,0 @@
import React from "react";
import { describe, expect, test } from "vitest";
import { render, screen } from "@testing-library/react";
import { MantineProvider } from "@mantine/core";
import FileEditorFileName from "@app/components/fileEditor/FileEditorFileName";
import type { StirlingFileStub } from "@app/types/fileContext";
import type { FileId } from "@app/types/file";
const buildFileStub = (
overrides: Partial<StirlingFileStub> = {},
): StirlingFileStub => ({
id: "file-1" as FileId,
name: "report.pdf",
type: "application/pdf",
size: 1024,
lastModified: 0,
isLeaf: true,
originalFileId: "file-1",
versionNumber: 1,
...overrides,
});
const renderName = (file: StirlingFileStub) =>
render(
<MantineProvider>
<FileEditorFileName file={file} />
</MantineProvider>,
);
describe("FileEditorFileName (desktop)", () => {
test("renders red 'not saved' indicator when file has no local path", () => {
renderName(buildFileStub());
const indicator = screen.getByLabelText("fileNotSavedToDisk");
expect(indicator).toBeInTheDocument();
expect(indicator).toHaveStyle({
backgroundColor: "var(--mantine-color-red-6)",
});
expect(screen.queryByLabelText("unsavedChanges")).toBeNull();
expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull();
});
test("renders yellow 'unsaved changes' indicator when file is dirty", () => {
renderName(
buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: true }),
);
const indicator = screen.getByLabelText("unsavedChanges");
expect(indicator).toBeInTheDocument();
expect(indicator).toHaveStyle({
backgroundColor: "var(--mantine-color-yellow-6)",
});
expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull();
expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull();
});
test("renders green 'saved' indicator when file is persisted and clean", () => {
renderName(
buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: false }),
);
const indicator = screen.getByLabelText("fileSavedToDisk");
expect(indicator).toBeInTheDocument();
expect(indicator).toHaveStyle({
backgroundColor: "var(--mantine-color-green-6)",
});
expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull();
expect(screen.queryByLabelText("unsavedChanges")).toBeNull();
});
test("renders the filename alongside the indicator", () => {
renderName(buildFileStub({ name: "invoice.pdf" }));
expect(screen.getByText("invoice.pdf")).toBeInTheDocument();
});
});
@@ -1,71 +0,0 @@
import React from "react";
import { Tooltip } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { StirlingFileStub } from "@app/types/fileContext";
import { PrivateContent } from "@app/components/shared/PrivateContent";
import { truncateCenter } from "@app/utils/textUtils";
interface FileEditorFileNameProps {
file: StirlingFileStub;
maxLength?: number;
}
const FileEditorFileName = ({
file,
maxLength = 40,
}: FileEditorFileNameProps) => {
const { t } = useTranslation();
return (
<>
<PrivateContent>{truncateCenter(file.name, maxLength)}</PrivateContent>
{!file.localFilePath && (
<Tooltip label={t("fileNotSavedToDisk", "Not saved to disk")}>
<span
style={{
display: "inline-block",
width: "6px",
height: "6px",
borderRadius: "50%",
backgroundColor: "var(--mantine-color-red-6)",
flexShrink: 0,
}}
aria-label={t("fileNotSavedToDisk", "Not saved to disk")}
/>
</Tooltip>
)}
{file.localFilePath && file.isDirty && (
<Tooltip label={t("unsavedChanges", "Unsaved changes")}>
<span
style={{
display: "inline-block",
width: "6px",
height: "6px",
borderRadius: "50%",
backgroundColor: "var(--mantine-color-yellow-6)",
flexShrink: 0,
}}
aria-label={t("unsavedChanges", "Unsaved changes")}
/>
</Tooltip>
)}
{file.localFilePath && !file.isDirty && (
<Tooltip label={t("fileSavedToDisk", "File saved to disk")}>
<span
style={{
display: "inline-block",
width: "6px",
height: "6px",
borderRadius: "50%",
backgroundColor: "var(--mantine-color-green-6)",
flexShrink: 0,
}}
aria-label={t("fileSavedToDisk", "File saved to disk")}
/>
</Tooltip>
)}
</>
);
};
export default FileEditorFileName;
@@ -0,0 +1,36 @@
import { Tooltip } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { StirlingFileStub } from "@app/types/fileContext";
import styles from "@app/components/fileEditor/FileEditorThumbnail.module.css";
interface FileEditorStatusDotProps {
file: StirlingFileStub;
}
export function FileEditorStatusDot({ file }: FileEditorStatusDotProps) {
const { t } = useTranslation();
const label = !file.localFilePath
? t("fileNotSavedToDisk", "Not saved to disk")
: file.isDirty
? t("unsavedChanges", "Unsaved changes")
: t("fileSavedToDisk", "Saved to disk");
const color = !file.localFilePath
? "var(--mantine-color-red-6)"
: file.isDirty
? "var(--mantine-color-yellow-6)"
: "var(--mantine-color-green-6)";
return (
<div className={styles.thumbBadgesRight}>
<Tooltip label={label}>
<span
className={styles.statusDot}
style={{ backgroundColor: color }}
aria-label={label}
/>
</Tooltip>
</div>
);
}
@@ -1,131 +0,0 @@
import { useState, useEffect, useMemo } from "react";
import { Box, Tooltip, rem } from "@mantine/core";
import { useTranslation } from "react-i18next";
import {
connectionModeService,
type ConnectionMode,
} from "@app/services/connectionModeService";
import {
selfHostedServerMonitor,
type SelfHostedServerState,
} from "@app/services/selfHostedServerMonitor";
import { useBackendHealth } from "@app/hooks/useBackendHealth";
import { OPEN_SIGN_IN_EVENT } from "@app/constants/signInEvents";
interface RightRailFooterExtensionsProps {
className?: string;
}
function ConnectionStatusDot() {
const { t } = useTranslation();
const [connectionMode, setConnectionMode] = useState<ConnectionMode | null>(
null,
);
const [selfHostedState, setSelfHostedState] = useState<SelfHostedServerState>(
() => selfHostedServerMonitor.getSnapshot(),
);
const { isOnline, checkHealth } = useBackendHealth();
useEffect(() => {
void connectionModeService.getCurrentMode().then(setConnectionMode);
const unsubscribe = connectionModeService.subscribeToModeChanges(
(config) => {
setConnectionMode(config.mode);
},
);
return unsubscribe;
}, []);
useEffect(() => {
return selfHostedServerMonitor.subscribe(setSelfHostedState);
}, []);
const { label, color } = useMemo(() => {
if (connectionMode === "saas") {
return {
label: t("connectionMode.status.saas", "Connected to Stirling Cloud"),
color: "#3b82f6",
};
}
if (connectionMode === "selfhosted") {
const serverOnline = selfHostedState.isOnline;
const serverChecking = selfHostedState.status === "checking";
const backendLabel = serverChecking
? t(
"connectionMode.status.selfhostedChecking",
"Connected to self-hosted server (checking...)",
)
: serverOnline
? t(
"connectionMode.status.selfhostedOnline",
"Connected to self-hosted server",
)
: t(
"connectionMode.status.selfhostedOffline",
"Self-hosted server unreachable",
);
return {
label: backendLabel,
color: serverChecking
? "#fcc419"
: serverOnline
? "#37b24d"
: "#e03131",
};
}
// local
return {
label: isOnline
? t("connectionMode.status.localOnline", "Offline mode running")
: t("connectionMode.status.localOffline", "Offline mode running"),
color: "#868e96",
};
}, [connectionMode, selfHostedState, isOnline, t]);
return (
<Tooltip label={label} position="left" offset={12} withArrow withinPortal>
<Box
component="span"
role="status"
aria-label={label}
tabIndex={0}
onClick={() => {
if (connectionMode === "local") {
window.dispatchEvent(new CustomEvent(OPEN_SIGN_IN_EVENT));
} else {
void checkHealth();
}
}}
style={{
width: rem(10),
height: rem(10),
borderRadius: "50%",
backgroundColor: color,
boxShadow: "var(--status-dot-ring)",
display: "inline-block",
cursor: "pointer",
outline: "none",
}}
/>
</Tooltip>
);
}
export function RightRailFooterExtensions({
className,
}: RightRailFooterExtensionsProps) {
return (
<Box
className={className}
style={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
paddingBottom: rem(12),
}}
>
<ConnectionStatusDot />
</Box>
);
}
@@ -23,6 +23,7 @@ export const useConfigNavSections = (
isAdmin: boolean = false,
runningEE: boolean = false,
loginEnabled: boolean = false,
onRequestClose: () => void = () => {},
): ConfigNavSection[] => {
const { t } = useTranslation();
@@ -52,6 +53,7 @@ export const useConfigNavSections = (
isAdmin,
runningEE,
loginEnabled,
onRequestClose,
);
const connectionModeSection: ConfigNavSection = {
@@ -4,7 +4,7 @@ import SaveOutlinedIcon from "@mui/icons-material/SaveOutlined";
/**
* File action icons for desktop builds.
* Overrides the core implementation with desktop-appropriate icons.
* The presence of `saveAsIconName` signals RightRail to show the Save As button.
* The presence of `saveAsIconName` signals the WorkbenchBar to show the Save As button.
*/
export function useFileActionIcons() {
return {
@@ -12,7 +12,7 @@ export function useFileActionIcons() {
download: SaveOutlinedIcon,
uploadIconName: "folder-rounded" as const,
downloadIconName: "save-rounded" as const,
// Returning this icon name causes RightRail to render the Save As button.
// Returning this icon name causes WorkbenchBar to render the Save As button.
// On desktop, downloadFile() without a localPath shows a native save dialog.
saveAsIconName: "save-as-rounded" as string | undefined,
};
@@ -19,7 +19,7 @@ export function useFileActionTerminology() {
mobileUpload: t("landing.mobileUpload", "Upload from Mobile"),
uploadFromComputer: t("landing.openFromComputer", "Open from computer"),
download: t("save", "Save"),
downloadAll: t("rightRail.saveAll", "Save All"),
downloadAll: t("workbenchBar.saveAll", "Save All"),
downloadSelected: t("fileManager.saveSelected", "Save Selected"),
downloadUnavailable: t("saveUnavailable", "Save unavailable for this item"),
noFilesInStorage: t(
@@ -5,7 +5,7 @@ import { downloadFile } from "@app/services/downloadService";
/**
* Desktop-only keyboard shortcut: Ctrl/Cmd+S to save selected files
* Only saves files that have a localFilePath (came from disk)
* Matches Right Rail button behavior: saves selected files if any, otherwise all files
* Matches WorkbenchBar button behavior: saves selected files if any, otherwise all files
*/
export function useSaveShortcut() {
const { selectors, state } = useFileState();