From e0902e3c27635e765458d5b3e698a0b078fd7ba1 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Tue, 14 Apr 2026 02:48:29 +1000 Subject: [PATCH] feat: demui the `` dependency (#24275) Adds a small LinearProgress (determinate + MUI-style dual-bar indeterminate) and uses it on workspace build progress; drops MUI and adds Tailwind keyframes + Storybook stories. - New component under `site/src/components/LinearProgress/` - `WorkspaceBuildProgress` switched off `@mui/material/LinearProgress` - Added `bar-indeterminate` / `bar-indeterminate-2` animation keyframes image --- .../LinearProgress/LinearProgress.stories.tsx | 80 +++++++++++++++++++ .../LinearProgress/LinearProgress.tsx | 56 +++++++++++++ .../WorkspacePage/WorkspaceBuildProgress.tsx | 51 +++--------- site/tailwind.config.js | 34 ++++++++ site/vite.config.mts | 1 - 5 files changed, 182 insertions(+), 40 deletions(-) create mode 100644 site/src/components/LinearProgress/LinearProgress.stories.tsx create mode 100644 site/src/components/LinearProgress/LinearProgress.tsx diff --git a/site/src/components/LinearProgress/LinearProgress.stories.tsx b/site/src/components/LinearProgress/LinearProgress.stories.tsx new file mode 100644 index 0000000000..426f3cea7b --- /dev/null +++ b/site/src/components/LinearProgress/LinearProgress.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useEffect, useState } from "react"; +import LinearProgress from "./LinearProgress"; + +const meta: Meta = { + title: "Components/LinearProgress", + component: LinearProgress, + args: { + variant: "determinate", + value: 40, + }, + argTypes: { + variant: { + control: "inline-radio", + options: ["determinate", "indeterminate"], + }, + value: { + control: { type: "range", min: 0, max: 100, step: 1 }, + if: { arg: "variant", eq: "determinate" }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Indeterminate: Story = { + args: { + variant: "indeterminate", + value: 0, + }, + parameters: { + chromatic: { disable: true }, + }, +}; + +export const Determinate: Story = { + args: { + variant: "determinate", + value: 62, + }, +}; + +export const DeterminateSamples: Story = { + render: () => ( +
+ {([0, 25, 50, 75, 100] as const).map((value) => ( +
+ {value}% + +
+ ))} +
+ ), +}; + +export const ControlledDeterminate: Story = { + render: function ControlledDeterminateRender() { + const [value, setValue] = useState(0); + useEffect(() => { + const id = window.setInterval(() => { + setValue((previous) => (previous >= 100 ? 0 : previous + 2)); + }, 120); + return () => window.clearInterval(id); + }, []); + return ( +
+ + {value}% + + +
+ ); + }, + parameters: { + chromatic: { disable: true }, + }, +}; diff --git a/site/src/components/LinearProgress/LinearProgress.tsx b/site/src/components/LinearProgress/LinearProgress.tsx new file mode 100644 index 0000000000..f8d809051f --- /dev/null +++ b/site/src/components/LinearProgress/LinearProgress.tsx @@ -0,0 +1,56 @@ +import type React from "react"; +import type { FC } from "react"; +import { cn } from "#/utils/cn"; + +type LinearProgressProps = React.ComponentProps<"div"> & { + value: number; + variant: "determinate" | "indeterminate"; +}; + +const LinearProgress: FC = ({ + value, + className, + variant, + ...props +}) => { + const isDeterminate = variant === "determinate"; + + return ( +
+ {!isDeterminate ? ( + <> +
+
+ + ) : ( +
+ )} +
+ ); +}; + +export default LinearProgress; diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx index 26b2477b18..488123f218 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx @@ -1,6 +1,3 @@ -import { css } from "@emotion/css"; -import type { Interpolation, Theme } from "@emotion/react"; -import LinearProgress from "@mui/material/LinearProgress"; import dayjs, { type Dayjs } from "dayjs"; import duration from "dayjs/plugin/duration"; import capitalize from "lodash/capitalize"; @@ -10,6 +7,7 @@ import type { TransitionStats, Workspace, } from "#/api/typesGenerated"; +import LinearProgress from "#/components/LinearProgress/LinearProgress"; dayjs.extend(duration); @@ -124,10 +122,13 @@ export const WorkspaceBuildProgress: FC = ({ return null; } return ( -
+
{variant === "task" && (
-
+
{progressText}
@@ -144,22 +145,16 @@ export const WorkspaceBuildProgress: FC = ({ ? "determinate" : "indeterminate" } - classes={{ - // If a transition is set, there is a moment on new load where the bar - // accelerates to progressValue and then rapidly decelerates, which is - // not indicative of true progress. - bar: classNames.bar, - // With the "task" variant, the progress bar is fullscreen, so remove - // the border radius. - root: variant === "task" ? classNames.root : undefined, - }} /> {variant !== "task" && ( -
-
+
+
{capitalize(workspace.latest_build.status)} workspace...
-
+
{progressText}
@@ -167,25 +162,3 @@ export const WorkspaceBuildProgress: FC = ({
); }; - -const classNames = { - bar: css` - transition: none; - `, - root: css` - border-radius: 0; - `, -}; - -const styles = { - stack: { - paddingLeft: 2, - paddingRight: 2, - }, - label: (theme) => ({ - fontSize: 12, - display: "block", - fontWeight: 600, - color: theme.palette.text.secondary, - }), -} satisfies Record>; diff --git a/site/tailwind.config.js b/site/tailwind.config.js index 65d0c87200..e6d81f0053 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -124,12 +124,46 @@ module.exports = { "30%": { left: "0%", width: "40%" }, "100%": { left: "100%", width: "0%" }, }, + // Matches MUI LinearProgress bar1/bar2 indeterminate keyframes; two + // staggered bars are required so one is visible while the other resets. + "bar-indeterminate": { + "0%": { + left: "-35%", + right: "100%", + }, + "60%": { + left: "100%", + right: "-90%", + }, + "100%": { + left: "100%", + right: "-90%", + }, + }, + "bar-indeterminate-2": { + "0%": { + left: "-200%", + right: "100%", + }, + "60%": { + left: "107%", + right: "-8%", + }, + "100%": { + left: "107%", + right: "-8%", + }, + }, }, animation: { loading: "loading 2s ease-in-out infinite alternate", "caret-scan": "caret-scan 3s ease-in-out infinite", "spin-once": "spin 1s cubic-bezier(0.4, 0, 0.2, 1)", "zip-right": "zip-right 1s cubic-bezier(0.4, 0, 0.2, 1)", + "bar-indeterminate": + "bar-indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite", + "bar-indeterminate-2": + "bar-indeterminate-2 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite", }, }, }, diff --git a/site/vite.config.mts b/site/vite.config.mts index 158a80349f..c85ef73371 100644 --- a/site/vite.config.mts +++ b/site/vite.config.mts @@ -182,7 +182,6 @@ export default defineConfig({ "@mui/material/FormLabel", "@mui/material/InputAdornment", "@mui/material/InputBase", - "@mui/material/LinearProgress", "@mui/material/Link", "@mui/material/List", "@mui/material/ListItem",